From 96c3472dce26bfc95cb6f3cab6bed511f8d73ab3 Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Mon, 25 Sep 2023 18:21:04 +0200 Subject: [PATCH 01/31] refactor(lexical-editor): add selection and element utilities --- apps/admin/src/App.tsx | 2 + apps/admin/src/StrikeThroughAction.tsx | 62 +++++++++++ .../LexicalCmsEditor/TypographyDropDown.tsx | 60 ++++++---- .../src/components/TypographyDropDown.tsx | 56 +++++----- .../src/components/Toolbar/StaticToolbar.tsx | 97 +--------------- .../src/components/Toolbar/Toolbar.tsx | 36 ++---- .../components/ToolbarActions/BoldAction.tsx | 18 +-- .../ToolbarActions/BulletListAction.tsx | 20 ++-- .../ToolbarActions/CodeHighlightAction.tsx | 18 +-- .../ToolbarActions/FontSizeAction.tsx | 2 +- .../ToolbarActions/ItalicAction.tsx | 18 +-- .../ToolbarActions/NumberedListAction.tsx | 24 ++-- .../components/ToolbarActions/QuoteAction.tsx | 20 ++-- .../ToolbarActions/TypographyAction.tsx | 105 +++++++++--------- .../ToolbarActions/UnderlineAction.tsx | 20 ++-- .../src/context/RichTextEditorContext.tsx | 2 +- .../src/hooks/useCurrentElement.ts | 26 +++++ .../src/hooks/useCurrentSelection.ts | 28 +++++ packages/lexical-editor/src/index.tsx | 1 + .../src/utils/getLexicalTextSelectionState.ts | 9 +- 20 files changed, 314 insertions(+), 310 deletions(-) create mode 100644 apps/admin/src/StrikeThroughAction.tsx create mode 100644 packages/lexical-editor/src/hooks/useCurrentElement.ts create mode 100644 packages/lexical-editor/src/hooks/useCurrentSelection.ts diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 0015d2d546b..0d72f30d55f 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -2,11 +2,13 @@ import React from "react"; import { Admin } from "@webiny/app-serverless-cms"; import { Cognito } from "@webiny/app-admin-users-cognito"; import "./App.scss"; +import { LexicalPlugins } from "./StrikeThroughAction"; export const App: React.FC = () => { return ( + ); }; diff --git a/apps/admin/src/StrikeThroughAction.tsx b/apps/admin/src/StrikeThroughAction.tsx new file mode 100644 index 00000000000..42a39ffbde7 --- /dev/null +++ b/apps/admin/src/StrikeThroughAction.tsx @@ -0,0 +1,62 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { LexicalEditorConfig } from "@webiny/app-page-builder"; +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import { FORMAT_TEXT_COMMAND, $getSelection, $isRangeSelection, $isNodeSelection } from "lexical"; + +function useEditorSelection() { + const [editor] = useLexicalComposerContext(); + const [selection, setSelection] = useState>(null); + + const storeSelection = useCallback(() => { + editor.getEditorState().read(() => { + setSelection($getSelection()); + }); + }, [editor]); + + useEffect(() => { + // On first mount, store current selection. + storeSelection(); + + // Subscribe to editor updates and keep track of the selection. + return editor.registerUpdateListener(storeSelection); + }, []); + + return { + selection, + rangeSelection: $isRangeSelection(selection) ? selection : null, + nodeSelection: $isNodeSelection(selection) ? selection : null + }; +} + +const StrikeThroughAction = () => { + const [editor] = useLexicalComposerContext(); + const { rangeSelection } = useEditorSelection(); + + const isStrikeThrough = rangeSelection ? rangeSelection.hasFormat("strikethrough") : false; + + const handleClick = () => { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough"); + }; + + return ( + + ); +}; + +export const LexicalPlugins = () => { + return ( + + } + /> + + ); +}; diff --git a/packages/app-headless-cms/src/admin/components/LexicalCmsEditor/TypographyDropDown.tsx b/packages/app-headless-cms/src/admin/components/LexicalCmsEditor/TypographyDropDown.tsx index 1f5a0257a59..0d8287317af 100644 --- a/packages/app-headless-cms/src/admin/components/LexicalCmsEditor/TypographyDropDown.tsx +++ b/packages/app-headless-cms/src/admin/components/LexicalCmsEditor/TypographyDropDown.tsx @@ -2,12 +2,18 @@ import React, { useEffect, useState } from "react"; import { DropDown, DropDownItem, - useRichTextEditor, + useCurrentSelection, useTypographyAction } from "@webiny/lexical-editor"; import { TypographyStyle } from "@webiny/theme/types"; import { TypographyValue } from "@webiny/lexical-editor/types"; import { useTheme } from "@webiny/app-admin"; +import { useCurrentElement } from "@webiny/lexical-editor/hooks/useCurrentElement"; +import { $isHeadingNode } from "@webiny/lexical-editor/nodes/HeadingNode"; +import { $isParagraphNode } from "@webiny/lexical-editor/nodes/ParagraphNode"; +import { $isQuoteNode } from "@webiny/lexical-editor/nodes/QuoteNode"; +import { $isListNode, ListNode } from "@webiny/lexical-editor/nodes/ListNode"; +import { $getNearestNodeOfType } from "@lexical/utils"; /* * This components support the typography selection for page builder and HCMS. * */ @@ -15,8 +21,8 @@ export const TypographyDropDown = () => { const { value, applyTypography } = useTypographyAction(); const { theme } = useTheme(); const [styles, setStyles] = useState([]); - const { textBlockSelection } = useRichTextEditor(); - const textType = textBlockSelection?.state?.textType; + const { element } = useCurrentElement(); + const { rangeSelection } = useCurrentSelection(); const getAllTextStyles = (): TypographyStyle[] => { if (!theme?.styles.typography) { @@ -46,26 +52,40 @@ export const TypographyDropDown = () => { }; useEffect(() => { - if (textType) { - switch (textType) { - case "heading": - case "paragraph": - setStyles(getAllTextStyles()); - break; - case "bullet": + if (!element || !rangeSelection) { + return; + } + + switch (true) { + case $isHeadingNode(element): + case $isParagraphNode(element): + setStyles(getAllTextStyles()); + break; + case $isListNode(element): + let type; + try { + const anchorNode = rangeSelection.anchor.getNode(); + const parentList = $getNearestNodeOfType(anchorNode, ListNode); + if (parentList) { + type = parentList.getListType(); + } + } catch { + type = element.getListType(); + } + + if (type === "bullet") { setStyles(getListStyles("ul")); - break; - case "number": + } else { setStyles(getListStyles("ol")); - break; - case "quoteblock": - setStyles(theme?.styles.typography?.quotes || []); - break; - default: - setStyles([]); - } + } + break; + case $isQuoteNode(element): + setStyles(theme?.styles.typography?.quotes || []); + break; + default: + setStyles([]); } - }, [textType]); + }, [element]); return ( <> diff --git a/packages/lexical-editor-pb-element/src/components/TypographyDropDown.tsx b/packages/lexical-editor-pb-element/src/components/TypographyDropDown.tsx index 5e9af7daebd..01619468d04 100644 --- a/packages/lexical-editor-pb-element/src/components/TypographyDropDown.tsx +++ b/packages/lexical-editor-pb-element/src/components/TypographyDropDown.tsx @@ -8,6 +8,10 @@ import { import { TypographyStyle } from "@webiny/theme/types"; import { TypographyValue } from "@webiny/lexical-editor/types"; import { useTheme } from "@webiny/app-admin"; +import { useCurrentElement } from "@webiny/lexical-editor/hooks/useCurrentElement"; +import { $isHeadingNode } from "@webiny/lexical-editor/nodes/HeadingNode"; +import { $isParagraphNode } from "@webiny/lexical-editor/nodes/ParagraphNode"; +import { $isQuoteNode } from "@webiny/lexical-editor/nodes/QuoteNode"; /* * This components support the typography selection for the Page Builder app. @@ -16,8 +20,7 @@ export const TypographyDropDown = () => { const { value, applyTypography } = useTypographyAction(); const { theme } = useTheme(); const [styles, setStyles] = useState([]); - const { textBlockSelection } = useRichTextEditor(); - const textType = textBlockSelection?.state?.textType; + const { element } = useCurrentElement(); const getListStyles = (tag: string): TypographyStyle[] => { const listStyles = theme?.styles.typography.lists?.filter(x => x.tag === tag) || []; @@ -30,30 +33,33 @@ export const TypographyDropDown = () => { }; useEffect(() => { - if (textType) { - switch (textType) { - case "heading": - const headingsStyles = theme?.styles.typography?.headings || []; - setStyles(headingsStyles); - break; - case "paragraph": - const paragraphStyles = theme?.styles.typography?.paragraphs || []; - setStyles(paragraphStyles); - break; - case "bullet": - setStyles(getListStyles("ul")); - break; - case "number": - setStyles(getListStyles("ol")); - break; - case "quoteblock": - setStyles(theme?.styles.typography?.quotes || []); - break; - default: - setStyles([]); - } + console.log("current element", element); + if (!element) { + return; } - }, [textType]); + + switch (true) { + case $isHeadingNode(element): + const headingsStyles = theme?.styles.typography?.headings || []; + setStyles(headingsStyles); + break; + case $isParagraphNode(element): + const paragraphStyles = theme?.styles.typography?.paragraphs || []; + setStyles(paragraphStyles); + break; + // case "bullet": + // setStyles(getListStyles("ul")); + // break; + // case "number": + // setStyles(getListStyles("ol")); + // break; + case $isQuoteNode(element): + setStyles(theme?.styles.typography?.quotes || []); + break; + default: + setStyles([]); + } + }, [element]); return ( <> diff --git a/packages/lexical-editor/src/components/Toolbar/StaticToolbar.tsx b/packages/lexical-editor/src/components/Toolbar/StaticToolbar.tsx index a2029e771f5..4df43e5d077 100644 --- a/packages/lexical-editor/src/components/Toolbar/StaticToolbar.tsx +++ b/packages/lexical-editor/src/components/Toolbar/StaticToolbar.tsx @@ -1,98 +1,12 @@ -import React, { FC, Fragment, useCallback, useEffect, useState } from "react"; -import { - $getSelection, - $isRangeSelection, - COMMAND_PRIORITY_CRITICAL, - LexicalEditor, - SELECTION_CHANGE_COMMAND -} from "lexical"; -import { useRichTextEditor } from "~/hooks/useRichTextEditor"; -import { mergeRegister } from "@lexical/utils"; +import React, { Fragment } from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import "./StaticToolbar.css"; -import { getLexicalTextSelectionState } from "~/utils/getLexicalTextSelectionState"; import { useLexicalEditorConfig } from "~/components/LexicalEditorConfig/LexicalEditorConfig"; -interface useStaticToolbarProps { - editor: LexicalEditor; -} - -const useStaticToolbar: FC = ({ editor }) => { - const { setNodeIsText, setTextBlockSelection, setActiveEditor } = useRichTextEditor(); - const [toolbarActiveEditor, setToolbarActiveEditor] = useState(editor); +export const StaticToolbar = () => { + const [editor] = useLexicalComposerContext(); const { toolbarElements } = useLexicalEditorConfig(); - const updateToolbar = useCallback(() => { - editor.getEditorState().read(() => { - // Should not to pop up the floating toolbar when using IME input - if (editor.isComposing()) { - return; - } - - const selection = $getSelection(); - - if ($isRangeSelection(selection)) { - const selectionState = getLexicalTextSelectionState(toolbarActiveEditor, selection); - if (selectionState) { - setTextBlockSelection(selectionState); - if ( - selectionState.selectedText !== "" && - !selectionState.state?.link.isSelected - ) { - setNodeIsText(true); - } else { - setNodeIsText(false); - } - } - } - - if (!$isRangeSelection(selection)) { - setNodeIsText(false); - return; - } - }); - }, [toolbarActiveEditor]); - - useEffect(() => { - document.addEventListener("selectionchange", updateToolbar); - return () => { - document.removeEventListener("selectionchange", updateToolbar); - }; - }, [updateToolbar]); - - useEffect(() => { - return editor.registerCommand( - SELECTION_CHANGE_COMMAND, - (_payload, newEditor) => { - updateToolbar(); - setToolbarActiveEditor(newEditor); - setActiveEditor(newEditor); - return false; - }, - COMMAND_PRIORITY_CRITICAL - ); - }, [editor, updateToolbar]); - - useEffect(() => { - return mergeRegister( - toolbarActiveEditor.registerUpdateListener(({ editorState }) => { - editorState.read(() => { - updateToolbar(); - }); - }), - editor.registerRootListener(() => { - if (editor.getRootElement() === null) { - setNodeIsText(false); - } - }), - editor.registerRootListener(() => { - if (editor.getRootElement() === null) { - setNodeIsText(false); - } - }) - ); - }, [updateToolbar, editor, toolbarActiveEditor]); - return (
{editor.isEditable() && ( @@ -105,8 +19,3 @@ const useStaticToolbar: FC = ({ editor }) => {
); }; - -export const StaticToolbar = () => { - const [editor] = useLexicalComposerContext(); - return useStaticToolbar({ editor }); -}; diff --git a/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx b/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx index f56d59dfb6f..06219df2f28 100644 --- a/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx +++ b/packages/lexical-editor/src/components/Toolbar/Toolbar.tsx @@ -1,4 +1,4 @@ -import React, { FC, Fragment, useCallback, useEffect, useRef, useState } from "react"; +import React, { FC, Fragment, useCallback, useEffect, useRef } from "react"; import { $getSelection, $isRangeSelection, @@ -151,18 +151,14 @@ const FloatingToolbar: FC = ({ anchorElem, editor }) => { ); }; -interface useToolbarProps { - editor: LexicalEditor; - anchorElem: HTMLElement; +export interface ToolbarProps { + anchorElem?: HTMLElement; } -const useToolbar: FC = ({ - editor, - anchorElem = document.body -}): JSX.Element | null => { +export const Toolbar = ({ anchorElem = document.body }: ToolbarProps) => { + const [editor] = useLexicalComposerContext(); const { nodeIsText, setNodeIsText, setActiveEditor, setIsEditable, setTextBlockSelection } = useRichTextEditor(); - const [toolbarActiveEditor, setToolbarActiveEditor] = useState(editor); const updateToolbar = useCallback(() => { editor.getEditorState().read(() => { @@ -174,7 +170,7 @@ const useToolbar: FC = ({ const selection = $getSelection(); if ($isRangeSelection(selection)) { - const selectionState = getLexicalTextSelectionState(toolbarActiveEditor, selection); + const selectionState = getLexicalTextSelectionState(editor, selection); if (selectionState) { setTextBlockSelection(selectionState); if ( @@ -193,7 +189,7 @@ const useToolbar: FC = ({ return; } }); - }, [toolbarActiveEditor]); + }, [editor]); useEffect(() => { document.addEventListener("selectionchange", updateToolbar); @@ -207,7 +203,6 @@ const useToolbar: FC = ({ SELECTION_CHANGE_COMMAND, (_payload, newEditor) => { updateToolbar(); - setToolbarActiveEditor(newEditor); setActiveEditor(newEditor); return false; }, @@ -220,31 +215,18 @@ const useToolbar: FC = ({ editor.registerEditableListener(editable => { setIsEditable(editable); }), - toolbarActiveEditor.registerUpdateListener(({ editorState }) => { - editorState.read(() => { - updateToolbar(); - }); - }), editor.registerRootListener(() => { if (editor.getRootElement() === null) { setNodeIsText(false); } }) ); - }, [updateToolbar, editor, toolbarActiveEditor]); + }, [updateToolbar, editor]); + // this is the only place where this var is used! REFACTOR! if (!nodeIsText) { return null; } return createPortal(, anchorElem); }; - -export interface ToolbarProps { - anchorElem?: HTMLElement; -} - -export const Toolbar = ({ anchorElem = document.body }: ToolbarProps) => { - const [editor] = useLexicalComposerContext(); - return useToolbar({ editor, anchorElem }); -}; diff --git a/packages/lexical-editor/src/components/ToolbarActions/BoldAction.tsx b/packages/lexical-editor/src/components/ToolbarActions/BoldAction.tsx index 01cf07f6e46..82feecada60 100644 --- a/packages/lexical-editor/src/components/ToolbarActions/BoldAction.tsx +++ b/packages/lexical-editor/src/components/ToolbarActions/BoldAction.tsx @@ -1,27 +1,21 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { FORMAT_TEXT_COMMAND } from "lexical"; -import { useRichTextEditor } from "~/hooks/useRichTextEditor"; +import { useCurrentSelection } from "~/hooks/useCurrentSelection"; export const BoldAction = () => { const [editor] = useLexicalComposerContext(); - const [isBold, setIsBold] = useState(false); - const { textBlockSelection } = useRichTextEditor(); - const isBoldSelected = !!textBlockSelection?.state?.bold; + const { rangeSelection } = useCurrentSelection(); + const isBoldSelected = rangeSelection ? rangeSelection.hasFormat("bold") : false; const handleClick = () => { editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"); - setIsBold(!isBold); }; - useEffect(() => { - setIsBold(isBoldSelected); - }, [isBoldSelected]); - return ( - ); -}; - -export const LexicalPlugins = () => { - return ( - - } - /> - - ); -}; diff --git a/packages/lexical-editor-pb-element/src/components/TypographyDropDown.tsx b/packages/lexical-editor-pb-element/src/components/TypographyDropDown.tsx index 74fae62631e..424f4553f71 100644 --- a/packages/lexical-editor-pb-element/src/components/TypographyDropDown.tsx +++ b/packages/lexical-editor-pb-element/src/components/TypographyDropDown.tsx @@ -1,8 +1,9 @@ import React, { useEffect, useState } from "react"; +import { $getNearestNodeOfType } from "@lexical/utils"; import { DropDown, DropDownItem, - useRichTextEditor, + useCurrentSelection, useTypographyAction } from "@webiny/lexical-editor"; import { TypographyStyle } from "@webiny/theme/types"; @@ -12,6 +13,7 @@ import { useCurrentElement } from "@webiny/lexical-editor/hooks/useCurrentElemen import { $isHeadingNode } from "@webiny/lexical-editor/nodes/HeadingNode"; import { $isParagraphNode } from "@webiny/lexical-editor/nodes/ParagraphNode"; import { $isQuoteNode } from "@webiny/lexical-editor/nodes/QuoteNode"; +import { $isListNode, ListNode } from "@webiny/lexical-editor/nodes/ListNode"; /* * This components support the typography selection for the Page Builder app. @@ -21,6 +23,7 @@ export const TypographyDropDown = () => { const { theme } = useTheme(); const [styles, setStyles] = useState([]); const { element } = useCurrentElement(); + const { rangeSelection } = useCurrentSelection(); const getListStyles = (tag: string): TypographyStyle[] => { const listStyles = theme?.styles.typography.lists?.filter(x => x.tag === tag) || []; @@ -33,7 +36,7 @@ export const TypographyDropDown = () => { }; useEffect(() => { - if (!element) { + if (!element || !rangeSelection) { return; } @@ -46,13 +49,24 @@ export const TypographyDropDown = () => { const paragraphStyles = theme?.styles.typography?.paragraphs || []; setStyles(paragraphStyles); break; - // TODO: finish these - // case "bullet": - // setStyles(getListStyles("ul")); - // break; - // case "number": - // setStyles(getListStyles("ol")); - // break; + case $isListNode(element): + let type; + try { + const anchorNode = rangeSelection.anchor.getNode(); + const parentList = $getNearestNodeOfType(anchorNode, ListNode); + if (parentList) { + type = parentList.getListType(); + } + } catch { + type = element.getListType(); + } + + if (type === "bullet") { + setStyles(getListStyles("ul")); + } else { + setStyles(getListStyles("ol")); + } + break; case $isQuoteNode(element): setStyles(theme?.styles.typography?.quotes || []); break; From a04816b2cdcb54993d04a3e66b5f1ecf85b0d0d4 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Mon, 2 Oct 2023 14:27:59 +0200 Subject: [PATCH 04/31] wip: html to lexical parser --- packages/api-form-builder/tsconfig.build.json | 2 +- packages/api-form-builder/tsconfig.json | 6 +- packages/html-to-lexical-parser/.babelrc.js | 1 + packages/html-to-lexical-parser/LICENSE | 21 +++ packages/html-to-lexical-parser/README.md | 15 ++ .../__tests__/html-articles.ts | 1 + .../__tests__/html-to-lexical.test.ts | 12 ++ .../html-to-lexical-parser/jest.config.js | 5 + packages/html-to-lexical-parser/package.json | 23 ++++ packages/html-to-lexical-parser/src/index.ts | 28 ++++ .../tsconfig.build.json | 12 ++ packages/html-to-lexical-parser/tsconfig.json | 17 +++ .../html-to-lexical-parser/webiny.config.js | 8 ++ packages/lexical-editor/src/index.tsx | 1 + .../src/utils/getSupportedNodeList.ts | 16 +++ yarn.lock | 130 +++++++++++++++++- 16 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 packages/html-to-lexical-parser/.babelrc.js create mode 100644 packages/html-to-lexical-parser/LICENSE create mode 100644 packages/html-to-lexical-parser/README.md create mode 100644 packages/html-to-lexical-parser/__tests__/html-articles.ts create mode 100644 packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts create mode 100644 packages/html-to-lexical-parser/jest.config.js create mode 100644 packages/html-to-lexical-parser/package.json create mode 100644 packages/html-to-lexical-parser/src/index.ts create mode 100644 packages/html-to-lexical-parser/tsconfig.build.json create mode 100644 packages/html-to-lexical-parser/tsconfig.json create mode 100644 packages/html-to-lexical-parser/webiny.config.js create mode 100644 packages/lexical-editor/src/utils/getSupportedNodeList.ts diff --git a/packages/api-form-builder/tsconfig.build.json b/packages/api-form-builder/tsconfig.build.json index 179e2d74073..7081630abf5 100644 --- a/packages/api-form-builder/tsconfig.build.json +++ b/packages/api-form-builder/tsconfig.build.json @@ -5,8 +5,8 @@ { "path": "../api/tsconfig.build.json" }, { "path": "../api-file-manager/tsconfig.build.json" }, { "path": "../api-i18n/tsconfig.build.json" }, - { "path": "../api-page-builder/tsconfig.build.json" }, { "path": "../api-mailer/tsconfig.build.json" }, + { "path": "../api-page-builder/tsconfig.build.json" }, { "path": "../api-security/tsconfig.build.json" }, { "path": "../api-tenancy/tsconfig.build.json" }, { "path": "../error/tsconfig.build.json" }, diff --git a/packages/api-form-builder/tsconfig.json b/packages/api-form-builder/tsconfig.json index 511bf7b4cb2..82227c57521 100644 --- a/packages/api-form-builder/tsconfig.json +++ b/packages/api-form-builder/tsconfig.json @@ -5,8 +5,8 @@ { "path": "../api" }, { "path": "../api-file-manager" }, { "path": "../api-i18n" }, - { "path": "../api-page-builder" }, { "path": "../api-mailer" }, + { "path": "../api-page-builder" }, { "path": "../api-security" }, { "path": "../api-tenancy" }, { "path": "../error" }, @@ -33,10 +33,10 @@ "@webiny/api-file-manager": ["../api-file-manager/src"], "@webiny/api-i18n/*": ["../api-i18n/src/*"], "@webiny/api-i18n": ["../api-i18n/src"], - "@webiny/api-page-builder/*": ["../api-page-builder/src/*"], - "@webiny/api-page-builder": ["../api-page-builder/src"], "@webiny/api-mailer/*": ["../api-mailer/src/*"], "@webiny/api-mailer": ["../api-mailer/src"], + "@webiny/api-page-builder/*": ["../api-page-builder/src/*"], + "@webiny/api-page-builder": ["../api-page-builder/src"], "@webiny/api-security/*": ["../api-security/src/*"], "@webiny/api-security": ["../api-security/src"], "@webiny/api-tenancy/*": ["../api-tenancy/src/*"], diff --git a/packages/html-to-lexical-parser/.babelrc.js b/packages/html-to-lexical-parser/.babelrc.js new file mode 100644 index 00000000000..bec58b263bd --- /dev/null +++ b/packages/html-to-lexical-parser/.babelrc.js @@ -0,0 +1 @@ +module.exports = require("@webiny/project-utils").createBabelConfigForReact({ path: __dirname }); diff --git a/packages/html-to-lexical-parser/LICENSE b/packages/html-to-lexical-parser/LICENSE new file mode 100644 index 00000000000..f772d04d4db --- /dev/null +++ b/packages/html-to-lexical-parser/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Webiny + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/html-to-lexical-parser/README.md b/packages/html-to-lexical-parser/README.md new file mode 100644 index 00000000000..623f87fe26d --- /dev/null +++ b/packages/html-to-lexical-parser/README.md @@ -0,0 +1,15 @@ +# @webiny/lexical-editor-actions +[![](https://img.shields.io/npm/dw/@webiny/lexical-editor-actions.svg)](https://www.npmjs.com/package/@webiny/lexical-editor) +[![](https://img.shields.io/npm/v/@webiny/lexical-editor-actions.svg)](https://www.npmjs.com/package/@webiny/lexical-editor) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) + + +## About + +This package provides actions plugins for Lexical editor. + + +## Where is it used? + +Currently, this packaged is used in [@webiny/app-serverless-cms](../app-serverless-cms). diff --git a/packages/html-to-lexical-parser/__tests__/html-articles.ts b/packages/html-to-lexical-parser/__tests__/html-articles.ts new file mode 100644 index 00000000000..32315922c8f --- /dev/null +++ b/packages/html-to-lexical-parser/__tests__/html-articles.ts @@ -0,0 +1 @@ +const carArticle = `

2024 Lexus TX Enters 3-Row SUV Price Brawl With Competitive MSRP

The all-new Lexus luxury SUV starts at a reasonable price for the segment.

Related Video

Lexus's first true midsize three-row SUV, the all-new TX, is almost here. With a properly boxy body atop car-based underpinnings, it's a far roomier offering than the half-baked, extended version of the last-generation two-row RX Lexus sold previously. It's also far more family friendly than the brand's body-on-frame, more-off-road-oriented GX and LX SUVs. Those may have had three rows of seating, but were aimed at a different swath of buyers than those shopping for mall crawlers like the Acura MDX, Audi Q7, Infiniti QX60, Volvo XC90, and so on. With the TX set to launch soon, we finally know how much it'll cost.

2024 Lexus TX Price? It's Nice

There will be several versions of the TX available at launch, with a third option coming later. The entry-level TX350 comes powered by a 275-hp turbocharged 2.4-liter four-cylinder engine; the TX500h F Sport Performance model uses a hybridized version of that 2.4-liter engine and produces a saucier 366 total hp; and coming later there is a TX550h+ plug-in hybrid that combines several electric motors with a 3.5-liter V-6 and delivers 406 hp.

Each of these TX variants is subdivided into narrower trim levels, with the TX350 available in base, Premium, and Luxury specifications, with each offered with front- or all-wheel-drive for an extra $1,600; the TX500h is only available with all-wheel-drive and in Premium and Luxury trims. TX550h+ details are forthcoming.

Lexus TX350 Pricing and Features

  • TX350: The least-expensive TX350, with front-wheel-drive and no options, starts at $55,050 including Lexus's rich $1,350 destination charge. Seven-passenger seating is standard, with eight-way power adjustment and heating on the fronts, and the cabin is slathered in Lexus's NuLuxe faux-leather. Proximity key access is included, as is a power-opening tailgate with hands-free sensor, manual rear side window sunshades, a wireless phone charger, 14-inch touchscreen, wireless Apple CarPlay and Android Auto, rain-sensing wipers, and Lexus's Safety System+ 3.0 active safety gear (adaptive cruise control, automated emergency braking, lane-keep assist, blind-spot monitoring, and auto high beams).
  • TX350 Premium: Besides opening access to a number of higher-end option packages, the Premium trim adds as standard unique 20-inch wheels, a panoramic sunroof, power-folding and reclining third-row seats, and front-seat ventilation and memory function. The price rises to $58,450, with AWD again a $1,600 option.
  • TX350 Luxury: The biggest changeover you'll see going from a TX350 Premium to Luxury model is leather. It replaces the faux stuff in the lesser 350s. Additional standard content includes "thematic ambient lighting," 10-way power adjustment for the front seats, heated outboard rear seats, a heated steering wheel, and headlight washers. If you want six-passenger seating with second-row captain's chairs, you'll need to upgrade to this Luxury spec, by the way. The price is $60,950, plus $1,600 for AWD.

TX500h F Sport Performance Pricing and Features

  • TX500h Premium: The TX500h comes standard with all-wheel drive and six-passenger seating, presumably in keeping with its zestier vibe relative to the other TXs, albeit slathered in NuLuxe faux leather. (You can't get a 500h with seven-passenger seating, in fact.) Lexus sprinkles a bunch of F Sport bits, from sport pedals to the steering wheel to the dark gray 22-inch wheels. It is otherwise equipped like the TX350 Luxury, plus heated and ventilated second-row seats, and starts at $69,350.
  • TX500h Luxury: For actual leather, you'll want the $72,650 TX500h Luxury, which replaces NuLuxe with real stuff. It also includes a heated steering wheel, panoramic sunroof, 21-speaker Mark Levinson audio system, and unique 22-inch wheels.

Option packages include the Technology package (12.3-inch gauge cluster screen, 360-degree camera, self-parking, digital rearview mirror, and head-up display, depending on the trim it's added to) for between $1,050 and $2,380; the $895 Convenience package (digital key, front cross-traffic alert, traffic jam assist); and $250 Cold Area package (windshield wiper de-icer, heated wheel on models not equipped).

Compared to the Acura MDX, Audi Q7, and Infiniti QX60, the TX is priced in the thick of things. The Acura starts at $51,045, the Infiniti at $50,845, and the Audi is way out there at $60,695. That puts the TX right in the mix as a compelling option for shoppers.

`; diff --git a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts new file mode 100644 index 00000000000..9cbe4ff5670 --- /dev/null +++ b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts @@ -0,0 +1,12 @@ +/** + * @jest-environment jsdom + */ + +import { parseToLexicalObject } from "~/index"; + +describe("Test html-to-lexical parser", () => { + it("should parse html string to lexical object", async () => { + const lexicalObject = parseToLexicalObject(carArticle); + expect(lexicalObject).toEqual({ test: "string" }); + }); +}); diff --git a/packages/html-to-lexical-parser/jest.config.js b/packages/html-to-lexical-parser/jest.config.js new file mode 100644 index 00000000000..cc5ac2bb64f --- /dev/null +++ b/packages/html-to-lexical-parser/jest.config.js @@ -0,0 +1,5 @@ +const base = require("../../jest.config.base"); + +module.exports = { + ...base({ path: __dirname }) +}; diff --git a/packages/html-to-lexical-parser/package.json b/packages/html-to-lexical-parser/package.json new file mode 100644 index 00000000000..38408c8c810 --- /dev/null +++ b/packages/html-to-lexical-parser/package.json @@ -0,0 +1,23 @@ +{ + "name": "@webiny/html-to-lexical-parser", + "version": "0.0.0", + "dependencies": { + "@lexical/headless": "^0.11.3", + "@lexical/html": "^0.11.3", + "@webiny/lexical-editor": "0.0.0", + "jsdom": "22.1.0" + }, + "devDependencies": { + "@types/jsdom": "21.1.3", + "@webiny/cli": "0.0.0", + "@webiny/project-utils": "0.0.0" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "scripts": { + "build": "yarn webiny run build", + "watch": "yarn webiny run watch" + } +} diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts new file mode 100644 index 00000000000..186320c1e58 --- /dev/null +++ b/packages/html-to-lexical-parser/src/index.ts @@ -0,0 +1,28 @@ +import { generateInitialLexicalValue, getSupportedNodeList } from "@webiny/lexical-editor"; +import { createHeadlessEditor } from "@lexical/headless"; +import { JSDOM } from "jsdom"; +import { $generateNodesFromDOM } from "@lexical/html"; + +/** + * Parse html string to lexical object. + * This parser by default uses the Webiny lexical nodes. + */ +export const parseToLexicalObject = ( + htmlString: string, + onError?: (onError: Error) => void +): Record => { + if (!htmlString?.length) { + return JSON.parse(generateInitialLexicalValue()); + } + + const editor = createHeadlessEditor({ + nodes: getSupportedNodeList(), + onError: onError + }); + + const dom = new JSDOM(htmlString); + + // Convert to lexical node objects format that can be stored in db. + const nodesData = $generateNodesFromDOM(editor, dom).map(node => node.get()); + return nodesData; +}; diff --git a/packages/html-to-lexical-parser/tsconfig.build.json b/packages/html-to-lexical-parser/tsconfig.build.json new file mode 100644 index 00000000000..7549e46bcdf --- /dev/null +++ b/packages/html-to-lexical-parser/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.build.json", + "include": ["src"], + "references": [{ "path": "../lexical-editor/tsconfig.build.json" }], + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "declarationDir": "./dist", + "paths": { "~/*": ["./src/*"], "~tests/*": ["./__tests__/*"] }, + "baseUrl": "." + } +} diff --git a/packages/html-to-lexical-parser/tsconfig.json b/packages/html-to-lexical-parser/tsconfig.json new file mode 100644 index 00000000000..a7101f93fe4 --- /dev/null +++ b/packages/html-to-lexical-parser/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "__tests__"], + "references": [{ "path": "../lexical-editor" }], + "compilerOptions": { + "rootDirs": ["./src", "./__tests__"], + "outDir": "./dist", + "declarationDir": "./dist", + "paths": { + "~/*": ["./src/*"], + "~tests/*": ["./__tests__/*"], + "@webiny/lexical-editor/*": ["../lexical-editor/src/*"], + "@webiny/lexical-editor": ["../lexical-editor/src"] + }, + "baseUrl": "." + } +} diff --git a/packages/html-to-lexical-parser/webiny.config.js b/packages/html-to-lexical-parser/webiny.config.js new file mode 100644 index 00000000000..6dff86766c9 --- /dev/null +++ b/packages/html-to-lexical-parser/webiny.config.js @@ -0,0 +1,8 @@ +const { createWatchPackage, createBuildPackage } = require("@webiny/project-utils"); + +module.exports = { + commands: { + build: createBuildPackage({ cwd: __dirname }), + watch: createWatchPackage({ cwd: __dirname }) + } +}; diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index 052150c3850..8972ca0d01e 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -45,6 +45,7 @@ export { ImagesPlugin } from "~/plugins/ImagesPlugin/ImagesPlugin"; export { generateInitialLexicalValue } from "~/utils/generateInitialLexicalValue"; export { isValidLexicalData } from "~/utils/isValidLexicalData"; export { clearNodeFormatting } from "~/utils/nodes/clearNodeFormating"; +export { getSupportedNodeList } from "~/utils/getSupportedNodeList"; // Commands export { INSERT_IMAGE_COMMAND } from "~/commands/insertFiles"; // types diff --git a/packages/lexical-editor/src/utils/getSupportedNodeList.ts b/packages/lexical-editor/src/utils/getSupportedNodeList.ts new file mode 100644 index 00000000000..33ecf23755e --- /dev/null +++ b/packages/lexical-editor/src/utils/getSupportedNodeList.ts @@ -0,0 +1,16 @@ +import { Klass, LexicalNode } from "lexical"; +import { WebinyNodes } from "~/nodes/webinyNodes"; + +/** + * Get the supported list of lexical nodes types. + * These nodes are initialized in the lexical editor, for the page builder and headless CMS apps. + */ +export const getSupportedNodeList = (): ReadonlyArray< + | Klass + | { + replace: Klass; + with: (node: InstanceType) => LexicalNode; + } +> => { + return [...WebinyNodes]; +}; diff --git a/yarn.lock b/yarn.lock index 126c451aac1..369265a8c9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7787,6 +7787,15 @@ __metadata: languageName: node linkType: hard +"@lexical/headless@npm:^0.11.3": + version: 0.11.3 + resolution: "@lexical/headless@npm:0.11.3" + peerDependencies: + lexical: 0.11.3 + checksum: 9bbabea45714a69deff7a6df7df55033c71904ffcee7398b8713e6178aace28f2fd75e111ca1f4113b13b2161498413ec82d90587f6289b35ab0de355494ebd2 + languageName: node + linkType: hard + "@lexical/history@npm:0.11.3, @lexical/history@npm:^0.11.3": version: 0.11.3 resolution: "@lexical/history@npm:0.11.3" @@ -7798,7 +7807,7 @@ __metadata: languageName: node linkType: hard -"@lexical/html@npm:0.11.3": +"@lexical/html@npm:0.11.3, @lexical/html@npm:^0.11.3": version: 0.11.3 resolution: "@lexical/html@npm:0.11.3" dependencies: @@ -16170,6 +16179,20 @@ __metadata: languageName: unknown linkType: soft +"@webiny/html-to-lexical-parser@workspace:packages/html-to-lexical-parser": + version: 0.0.0-use.local + resolution: "@webiny/html-to-lexical-parser@workspace:packages/html-to-lexical-parser" + dependencies: + "@lexical/headless": ^0.11.3 + "@lexical/html": ^0.11.3 + "@types/jsdom": 21.1.3 + "@webiny/cli": 0.0.0 + "@webiny/lexical-editor": 0.0.0 + "@webiny/project-utils": 0.0.0 + jsdom: 22.1.0 + languageName: unknown + linkType: soft + "@webiny/i18n-react@0.0.0, @webiny/i18n-react@workspace:packages/i18n-react": version: 0.0.0-use.local resolution: "@webiny/i18n-react@workspace:packages/i18n-react" @@ -22460,6 +22483,15 @@ __metadata: languageName: node linkType: hard +"cssstyle@npm:^3.0.0": + version: 3.0.0 + resolution: "cssstyle@npm:3.0.0" + dependencies: + rrweb-cssom: ^0.6.0 + checksum: 31f694dfed9998ed93570fe539610837b878193dd8487c33cb12db8004333c53c2a3904166288bbec68388c72fb01014d46d3243ddfb02fe845989d852c06f27 + languageName: node + linkType: hard + "csstype@npm:^2.5.7, csstype@npm:^2.6.6": version: 2.6.21 resolution: "csstype@npm:2.6.21" @@ -22700,6 +22732,17 @@ __metadata: languageName: node linkType: hard +"data-urls@npm:^4.0.0": + version: 4.0.0 + resolution: "data-urls@npm:4.0.0" + dependencies: + abab: ^2.0.6 + whatwg-mimetype: ^3.0.0 + whatwg-url: ^12.0.0 + checksum: 006e869b5bf079647949a3e9b1dd69d84b2d5d26e6b01c265485699bc96e83817d4b5aae758b2910a4c58c0601913f3a0034121c1ca2da268e9a244c57515b15 + languageName: node + linkType: hard + "dataloader@npm:^2.0.0": version: 2.2.1 resolution: "dataloader@npm:2.2.1" @@ -22863,7 +22906,7 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:^10.4.2": +"decimal.js@npm:^10.4.2, decimal.js@npm:^10.4.3": version: 10.4.3 resolution: "decimal.js@npm:10.4.3" checksum: 796404dcfa9d1dbfdc48870229d57f788b48c21c603c3f6554a1c17c10195fc1024de338b0cf9e1efe0c7c167eeb18f04548979bcc5fdfabebb7cc0ae3287bae @@ -30632,6 +30675,42 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:22.1.0": + version: 22.1.0 + resolution: "jsdom@npm:22.1.0" + dependencies: + abab: ^2.0.6 + cssstyle: ^3.0.0 + data-urls: ^4.0.0 + decimal.js: ^10.4.3 + domexception: ^4.0.0 + form-data: ^4.0.0 + html-encoding-sniffer: ^3.0.0 + http-proxy-agent: ^5.0.0 + https-proxy-agent: ^5.0.1 + is-potential-custom-element-name: ^1.0.1 + nwsapi: ^2.2.4 + parse5: ^7.1.2 + rrweb-cssom: ^0.6.0 + saxes: ^6.0.0 + symbol-tree: ^3.2.4 + tough-cookie: ^4.1.2 + w3c-xmlserializer: ^4.0.0 + webidl-conversions: ^7.0.0 + whatwg-encoding: ^2.0.0 + whatwg-mimetype: ^3.0.0 + whatwg-url: ^12.0.1 + ws: ^8.13.0 + xml-name-validator: ^4.0.0 + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: d955ab83a6dad3e6af444098d30647c719bbb4cf97de053aa5751c03c8d6f3283d8c4d7fc2774c181f1d432fb0250e7332bc159e6b466424f4e337d73adcbf30 + languageName: node + linkType: hard + "jsdom@npm:^20.0.0": version: 20.0.3 resolution: "jsdom@npm:20.0.3" @@ -34034,7 +34113,7 @@ __metadata: languageName: node linkType: hard -"nwsapi@npm:^2.2.0": +"nwsapi@npm:^2.2.0, nwsapi@npm:^2.2.4": version: 2.2.7 resolution: "nwsapi@npm:2.2.7" checksum: cab25f7983acec7e23490fec3ef7be608041b460504229770e3bfcf9977c41d6fe58f518994d3bd9aa3a101f501089a3d4a63536f4ff8ae4b8c4ca23bdbfda4e @@ -34945,7 +35024,7 @@ __metadata: languageName: node linkType: hard -"parse5@npm:^7.0.0, parse5@npm:^7.1.1": +"parse5@npm:^7.0.0, parse5@npm:^7.1.1, parse5@npm:^7.1.2": version: 7.1.2 resolution: "parse5@npm:7.1.2" dependencies: @@ -36936,7 +37015,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0, punycode@npm:^2.1.1": +"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.0": version: 2.3.0 resolution: "punycode@npm:2.3.0" checksum: 39f760e09a2a3bbfe8f5287cf733ecdad69d6af2fe6f97ca95f24b8921858b91e9ea3c9eeec6e08cede96181b3bb33f95c6ffd8c77e63986508aa2e8159fa200 @@ -39312,6 +39391,13 @@ __metadata: languageName: unknown linkType: soft +"rrweb-cssom@npm:^0.6.0": + version: 0.6.0 + resolution: "rrweb-cssom@npm:0.6.0" + checksum: 182312f6e4f41d18230ccc34f14263bc8e8a6b9d30ee3ec0d2d8e643c6f27964cd7a8d638d4a00e988d93e8dc55369f4ab5a473ccfeff7a8bab95b36d2b5499c + languageName: node + linkType: hard + "run-async@npm:^2.2.0, run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -42089,6 +42175,15 @@ __metadata: languageName: node linkType: hard +"tr46@npm:^4.1.1": + version: 4.1.1 + resolution: "tr46@npm:4.1.1" + dependencies: + punycode: ^2.3.0 + checksum: aeeb821ac2cd792e63ec84888b4fd6598ac6ed75d861579e21a5cf9d4ee78b2c6b94e7d45036f2ca2088bc85b9b46560ad23c4482979421063b24137349dbd96 + languageName: node + linkType: hard + "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -43792,6 +43887,16 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^12.0.0, whatwg-url@npm:^12.0.1": + version: 12.0.1 + resolution: "whatwg-url@npm:12.0.1" + dependencies: + tr46: ^4.1.1 + webidl-conversions: ^7.0.0 + checksum: 8698993b763c1e7eda5ed16c31dab24bca6489626aca7caf8b5a2b64684dda6578194786f10ec42ceb1c175feea16d0a915096e6419e08d154ce551c43176972 + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" @@ -44153,6 +44258,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.13.0": + version: 8.14.2 + resolution: "ws@npm:8.14.2" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 3ca0dad26e8cc6515ff392b622a1467430814c463b3368b0258e33696b1d4bed7510bc7030f7b72838b9fdeb8dbd8839cbf808367d6aae2e1d668ce741d4308b + languageName: node + linkType: hard + "ws@npm:~7.2.0": version: 7.2.5 resolution: "ws@npm:7.2.5" From f08a2ee76b2ec23df6d40420b1da799bd6e19584 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Mon, 2 Oct 2023 15:32:59 +0200 Subject: [PATCH 05/31] wip: fixed problem with jsdom in tests --- .../__tests__/html-articles.ts | 2 +- .../__tests__/html-to-lexical.test.ts | 1 + .../__tests__/setupEnv.ts | 6 ++++++ packages/html-to-lexical-parser/jest.config.js | 6 +++++- packages/html-to-lexical-parser/package.json | 3 ++- packages/html-to-lexical-parser/src/index.ts | 18 ++++++++++++++---- packages/lexical-editor/src/index.tsx | 1 + packages/lexical-editor/src/utils/getTheme.ts | 9 +++++++++ 8 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 packages/html-to-lexical-parser/__tests__/setupEnv.ts create mode 100644 packages/lexical-editor/src/utils/getTheme.ts diff --git a/packages/html-to-lexical-parser/__tests__/html-articles.ts b/packages/html-to-lexical-parser/__tests__/html-articles.ts index 32315922c8f..60a74fa593f 100644 --- a/packages/html-to-lexical-parser/__tests__/html-articles.ts +++ b/packages/html-to-lexical-parser/__tests__/html-articles.ts @@ -1 +1 @@ -const carArticle = `

2024 Lexus TX Enters 3-Row SUV Price Brawl With Competitive MSRP

The all-new Lexus luxury SUV starts at a reasonable price for the segment.

Related Video

Lexus's first true midsize three-row SUV, the all-new TX, is almost here. With a properly boxy body atop car-based underpinnings, it's a far roomier offering than the half-baked, extended version of the last-generation two-row RX Lexus sold previously. It's also far more family friendly than the brand's body-on-frame, more-off-road-oriented GX and LX SUVs. Those may have had three rows of seating, but were aimed at a different swath of buyers than those shopping for mall crawlers like the Acura MDX, Audi Q7, Infiniti QX60, Volvo XC90, and so on. With the TX set to launch soon, we finally know how much it'll cost.

2024 Lexus TX Price? It's Nice

There will be several versions of the TX available at launch, with a third option coming later. The entry-level TX350 comes powered by a 275-hp turbocharged 2.4-liter four-cylinder engine; the TX500h F Sport Performance model uses a hybridized version of that 2.4-liter engine and produces a saucier 366 total hp; and coming later there is a TX550h+ plug-in hybrid that combines several electric motors with a 3.5-liter V-6 and delivers 406 hp.

Each of these TX variants is subdivided into narrower trim levels, with the TX350 available in base, Premium, and Luxury specifications, with each offered with front- or all-wheel-drive for an extra $1,600; the TX500h is only available with all-wheel-drive and in Premium and Luxury trims. TX550h+ details are forthcoming.

Lexus TX350 Pricing and Features

  • TX350: The least-expensive TX350, with front-wheel-drive and no options, starts at $55,050 including Lexus's rich $1,350 destination charge. Seven-passenger seating is standard, with eight-way power adjustment and heating on the fronts, and the cabin is slathered in Lexus's NuLuxe faux-leather. Proximity key access is included, as is a power-opening tailgate with hands-free sensor, manual rear side window sunshades, a wireless phone charger, 14-inch touchscreen, wireless Apple CarPlay and Android Auto, rain-sensing wipers, and Lexus's Safety System+ 3.0 active safety gear (adaptive cruise control, automated emergency braking, lane-keep assist, blind-spot monitoring, and auto high beams).
  • TX350 Premium: Besides opening access to a number of higher-end option packages, the Premium trim adds as standard unique 20-inch wheels, a panoramic sunroof, power-folding and reclining third-row seats, and front-seat ventilation and memory function. The price rises to $58,450, with AWD again a $1,600 option.
  • TX350 Luxury: The biggest changeover you'll see going from a TX350 Premium to Luxury model is leather. It replaces the faux stuff in the lesser 350s. Additional standard content includes "thematic ambient lighting," 10-way power adjustment for the front seats, heated outboard rear seats, a heated steering wheel, and headlight washers. If you want six-passenger seating with second-row captain's chairs, you'll need to upgrade to this Luxury spec, by the way. The price is $60,950, plus $1,600 for AWD.

TX500h F Sport Performance Pricing and Features

  • TX500h Premium: The TX500h comes standard with all-wheel drive and six-passenger seating, presumably in keeping with its zestier vibe relative to the other TXs, albeit slathered in NuLuxe faux leather. (You can't get a 500h with seven-passenger seating, in fact.) Lexus sprinkles a bunch of F Sport bits, from sport pedals to the steering wheel to the dark gray 22-inch wheels. It is otherwise equipped like the TX350 Luxury, plus heated and ventilated second-row seats, and starts at $69,350.
  • TX500h Luxury: For actual leather, you'll want the $72,650 TX500h Luxury, which replaces NuLuxe with real stuff. It also includes a heated steering wheel, panoramic sunroof, 21-speaker Mark Levinson audio system, and unique 22-inch wheels.

Option packages include the Technology package (12.3-inch gauge cluster screen, 360-degree camera, self-parking, digital rearview mirror, and head-up display, depending on the trim it's added to) for between $1,050 and $2,380; the $895 Convenience package (digital key, front cross-traffic alert, traffic jam assist); and $250 Cold Area package (windshield wiper de-icer, heated wheel on models not equipped).

Compared to the Acura MDX, Audi Q7, and Infiniti QX60, the TX is priced in the thick of things. The Acura starts at $51,045, the Infiniti at $50,845, and the Audi is way out there at $60,695. That puts the TX right in the mix as a compelling option for shoppers.

`; +export const carArticle = `

2024 Lexus TX Enters 3-Row SUV Price Brawl With Competitive MSRP

The all-new Lexus luxury SUV starts at a reasonable price for the segment.

Related Video

Lexus's first true midsize three-row SUV, the all-new TX, is almost here. With a properly boxy body atop car-based underpinnings, it's a far roomier offering than the half-baked, extended version of the last-generation two-row RX Lexus sold previously. It's also far more family friendly than the brand's body-on-frame, more-off-road-oriented GX and LX SUVs. Those may have had three rows of seating, but were aimed at a different swath of buyers than those shopping for mall crawlers like the Acura MDX, Audi Q7, Infiniti QX60, Volvo XC90, and so on. With the TX set to launch soon, we finally know how much it'll cost.

2024 Lexus TX Price? It's Nice

There will be several versions of the TX available at launch, with a third option coming later. The entry-level TX350 comes powered by a 275-hp turbocharged 2.4-liter four-cylinder engine; the TX500h F Sport Performance model uses a hybridized version of that 2.4-liter engine and produces a saucier 366 total hp; and coming later there is a TX550h+ plug-in hybrid that combines several electric motors with a 3.5-liter V-6 and delivers 406 hp.

Each of these TX variants is subdivided into narrower trim levels, with the TX350 available in base, Premium, and Luxury specifications, with each offered with front- or all-wheel-drive for an extra $1,600; the TX500h is only available with all-wheel-drive and in Premium and Luxury trims. TX550h+ details are forthcoming.

Lexus TX350 Pricing and Features

  • TX350: The least-expensive TX350, with front-wheel-drive and no options, starts at $55,050 including Lexus's rich $1,350 destination charge. Seven-passenger seating is standard, with eight-way power adjustment and heating on the fronts, and the cabin is slathered in Lexus's NuLuxe faux-leather. Proximity key access is included, as is a power-opening tailgate with hands-free sensor, manual rear side window sunshades, a wireless phone charger, 14-inch touchscreen, wireless Apple CarPlay and Android Auto, rain-sensing wipers, and Lexus's Safety System+ 3.0 active safety gear (adaptive cruise control, automated emergency braking, lane-keep assist, blind-spot monitoring, and auto high beams).
  • TX350 Premium: Besides opening access to a number of higher-end option packages, the Premium trim adds as standard unique 20-inch wheels, a panoramic sunroof, power-folding and reclining third-row seats, and front-seat ventilation and memory function. The price rises to $58,450, with AWD again a $1,600 option.
  • TX350 Luxury: The biggest changeover you'll see going from a TX350 Premium to Luxury model is leather. It replaces the faux stuff in the lesser 350s. Additional standard content includes "thematic ambient lighting," 10-way power adjustment for the front seats, heated outboard rear seats, a heated steering wheel, and headlight washers. If you want six-passenger seating with second-row captain's chairs, you'll need to upgrade to this Luxury spec, by the way. The price is $60,950, plus $1,600 for AWD.

TX500h F Sport Performance Pricing and Features

  • TX500h Premium: The TX500h comes standard with all-wheel drive and six-passenger seating, presumably in keeping with its zestier vibe relative to the other TXs, albeit slathered in NuLuxe faux leather. (You can't get a 500h with seven-passenger seating, in fact.) Lexus sprinkles a bunch of F Sport bits, from sport pedals to the steering wheel to the dark gray 22-inch wheels. It is otherwise equipped like the TX350 Luxury, plus heated and ventilated second-row seats, and starts at $69,350.
  • TX500h Luxury: For actual leather, you'll want the $72,650 TX500h Luxury, which replaces NuLuxe with real stuff. It also includes a heated steering wheel, panoramic sunroof, 21-speaker Mark Levinson audio system, and unique 22-inch wheels.

Option packages include the Technology package (12.3-inch gauge cluster screen, 360-degree camera, self-parking, digital rearview mirror, and head-up display, depending on the trim it's added to) for between $1,050 and $2,380; the $895 Convenience package (digital key, front cross-traffic alert, traffic jam assist); and $250 Cold Area package (windshield wiper de-icer, heated wheel on models not equipped).

Compared to the Acura MDX, Audi Q7, and Infiniti QX60, the TX is priced in the thick of things. The Acura starts at $51,045, the Infiniti at $50,845, and the Audi is way out there at $60,695. That puts the TX right in the mix as a compelling option for shoppers.

`; diff --git a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts index 9cbe4ff5670..2bbee2c7968 100644 --- a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts +++ b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts @@ -3,6 +3,7 @@ */ import { parseToLexicalObject } from "~/index"; +import { carArticle } from "./html-articles"; describe("Test html-to-lexical parser", () => { it("should parse html string to lexical object", async () => { diff --git a/packages/html-to-lexical-parser/__tests__/setupEnv.ts b/packages/html-to-lexical-parser/__tests__/setupEnv.ts new file mode 100644 index 00000000000..3e7eb83fb5c --- /dev/null +++ b/packages/html-to-lexical-parser/__tests__/setupEnv.ts @@ -0,0 +1,6 @@ +// @ts-nocheck +// noinspection JSConstantReassignment +const { TextEncoder, TextDecoder } = require("util"); + +global.TextEncoder = TextEncoder; +global.TextDecoder = TextDecoder; diff --git a/packages/html-to-lexical-parser/jest.config.js b/packages/html-to-lexical-parser/jest.config.js index cc5ac2bb64f..76a10e7fc7d 100644 --- a/packages/html-to-lexical-parser/jest.config.js +++ b/packages/html-to-lexical-parser/jest.config.js @@ -1,5 +1,9 @@ const base = require("../../jest.config.base"); module.exports = { - ...base({ path: __dirname }) + ...base({ path: __dirname }), + moduleNameMapper: { + "\\.(css|sass)$": "identity-obj-proxy" + }, + setupFilesAfterEnv: [require.resolve("./__tests__/setupEnv.ts")] }; diff --git a/packages/html-to-lexical-parser/package.json b/packages/html-to-lexical-parser/package.json index 38408c8c810..d9a0072cd70 100644 --- a/packages/html-to-lexical-parser/package.json +++ b/packages/html-to-lexical-parser/package.json @@ -19,5 +19,6 @@ "scripts": { "build": "yarn webiny run build", "watch": "yarn webiny run watch" - } + }, + "test": "jest --verbose --runInBand --detectOpenHandles --forceExit" } diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts index 186320c1e58..54362eb1170 100644 --- a/packages/html-to-lexical-parser/src/index.ts +++ b/packages/html-to-lexical-parser/src/index.ts @@ -1,7 +1,16 @@ -import { generateInitialLexicalValue, getSupportedNodeList } from "@webiny/lexical-editor"; +import { + generateInitialLexicalValue, + getSupportedNodeList, + getTheme +} from "@webiny/lexical-editor"; import { createHeadlessEditor } from "@lexical/headless"; -import { JSDOM } from "jsdom"; import { $generateNodesFromDOM } from "@lexical/html"; +import { TextEncoder } from "util"; + +const jsdom = require("jsdom"); +const { JSDOM } = jsdom; + +global.TextEncoder = TextEncoder; /** * Parse html string to lexical object. @@ -17,12 +26,13 @@ export const parseToLexicalObject = ( const editor = createHeadlessEditor({ nodes: getSupportedNodeList(), - onError: onError + onError: onError, + theme: getTheme() }); const dom = new JSDOM(htmlString); // Convert to lexical node objects format that can be stored in db. - const nodesData = $generateNodesFromDOM(editor, dom).map(node => node.get()); + const nodesData = $generateNodesFromDOM(editor, dom).map(node => node.exportJSON()); return nodesData; }; diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index 8972ca0d01e..550958fa13e 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -46,6 +46,7 @@ export { generateInitialLexicalValue } from "~/utils/generateInitialLexicalValue export { isValidLexicalData } from "~/utils/isValidLexicalData"; export { clearNodeFormatting } from "~/utils/nodes/clearNodeFormating"; export { getSupportedNodeList } from "~/utils/getSupportedNodeList"; +export { getTheme } from "~/utils/getTheme"; // Commands export { INSERT_IMAGE_COMMAND } from "~/commands/insertFiles"; // types diff --git a/packages/lexical-editor/src/utils/getTheme.ts b/packages/lexical-editor/src/utils/getTheme.ts new file mode 100644 index 00000000000..2d6d33334a1 --- /dev/null +++ b/packages/lexical-editor/src/utils/getTheme.ts @@ -0,0 +1,9 @@ +import { WebinyEditorTheme, webinyEditorTheme } from "~/themes/webinyLexicalTheme"; +import { EditorThemeClasses } from "lexical"; + +/** + * Get webiny theme used for lexical editor + */ +export const getTheme = (): EditorThemeClasses | WebinyEditorTheme => { + return webinyEditorTheme; +}; From ccb1598373ece95037a35f5739a29b71d624d39b Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Tue, 3 Oct 2023 13:36:17 +0200 Subject: [PATCH 06/31] wip: firs working version - parsing html to webiny nodes --- .../__tests__/html-articles.ts | 1 + .../__tests__/html-to-lexical.test.ts | 6 +++-- packages/html-to-lexical-parser/package.json | 3 ++- packages/html-to-lexical-parser/src/index.ts | 24 ++++++++++++------- yarn.lock | 1 + 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/html-to-lexical-parser/__tests__/html-articles.ts b/packages/html-to-lexical-parser/__tests__/html-articles.ts index 60a74fa593f..3466a756535 100644 --- a/packages/html-to-lexical-parser/__tests__/html-articles.ts +++ b/packages/html-to-lexical-parser/__tests__/html-articles.ts @@ -1 +1,2 @@ export const carArticle = `

2024 Lexus TX Enters 3-Row SUV Price Brawl With Competitive MSRP

The all-new Lexus luxury SUV starts at a reasonable price for the segment.

Related Video

Lexus's first true midsize three-row SUV, the all-new TX, is almost here. With a properly boxy body atop car-based underpinnings, it's a far roomier offering than the half-baked, extended version of the last-generation two-row RX Lexus sold previously. It's also far more family friendly than the brand's body-on-frame, more-off-road-oriented GX and LX SUVs. Those may have had three rows of seating, but were aimed at a different swath of buyers than those shopping for mall crawlers like the Acura MDX, Audi Q7, Infiniti QX60, Volvo XC90, and so on. With the TX set to launch soon, we finally know how much it'll cost.

2024 Lexus TX Price? It's Nice

There will be several versions of the TX available at launch, with a third option coming later. The entry-level TX350 comes powered by a 275-hp turbocharged 2.4-liter four-cylinder engine; the TX500h F Sport Performance model uses a hybridized version of that 2.4-liter engine and produces a saucier 366 total hp; and coming later there is a TX550h+ plug-in hybrid that combines several electric motors with a 3.5-liter V-6 and delivers 406 hp.

Each of these TX variants is subdivided into narrower trim levels, with the TX350 available in base, Premium, and Luxury specifications, with each offered with front- or all-wheel-drive for an extra $1,600; the TX500h is only available with all-wheel-drive and in Premium and Luxury trims. TX550h+ details are forthcoming.

Lexus TX350 Pricing and Features

  • TX350: The least-expensive TX350, with front-wheel-drive and no options, starts at $55,050 including Lexus's rich $1,350 destination charge. Seven-passenger seating is standard, with eight-way power adjustment and heating on the fronts, and the cabin is slathered in Lexus's NuLuxe faux-leather. Proximity key access is included, as is a power-opening tailgate with hands-free sensor, manual rear side window sunshades, a wireless phone charger, 14-inch touchscreen, wireless Apple CarPlay and Android Auto, rain-sensing wipers, and Lexus's Safety System+ 3.0 active safety gear (adaptive cruise control, automated emergency braking, lane-keep assist, blind-spot monitoring, and auto high beams).
  • TX350 Premium: Besides opening access to a number of higher-end option packages, the Premium trim adds as standard unique 20-inch wheels, a panoramic sunroof, power-folding and reclining third-row seats, and front-seat ventilation and memory function. The price rises to $58,450, with AWD again a $1,600 option.
  • TX350 Luxury: The biggest changeover you'll see going from a TX350 Premium to Luxury model is leather. It replaces the faux stuff in the lesser 350s. Additional standard content includes "thematic ambient lighting," 10-way power adjustment for the front seats, heated outboard rear seats, a heated steering wheel, and headlight washers. If you want six-passenger seating with second-row captain's chairs, you'll need to upgrade to this Luxury spec, by the way. The price is $60,950, plus $1,600 for AWD.

TX500h F Sport Performance Pricing and Features

  • TX500h Premium: The TX500h comes standard with all-wheel drive and six-passenger seating, presumably in keeping with its zestier vibe relative to the other TXs, albeit slathered in NuLuxe faux leather. (You can't get a 500h with seven-passenger seating, in fact.) Lexus sprinkles a bunch of F Sport bits, from sport pedals to the steering wheel to the dark gray 22-inch wheels. It is otherwise equipped like the TX350 Luxury, plus heated and ventilated second-row seats, and starts at $69,350.
  • TX500h Luxury: For actual leather, you'll want the $72,650 TX500h Luxury, which replaces NuLuxe with real stuff. It also includes a heated steering wheel, panoramic sunroof, 21-speaker Mark Levinson audio system, and unique 22-inch wheels.

Option packages include the Technology package (12.3-inch gauge cluster screen, 360-degree camera, self-parking, digital rearview mirror, and head-up display, depending on the trim it's added to) for between $1,050 and $2,380; the $895 Convenience package (digital key, front cross-traffic alert, traffic jam assist); and $250 Cold Area package (windshield wiper de-icer, heated wheel on models not equipped).

Compared to the Acura MDX, Audi Q7, and Infiniti QX60, the TX is priced in the thick of things. The Acura starts at $51,045, the Infiniti at $50,845, and the Audi is way out there at $60,695. That puts the TX right in the mix as a compelling option for shoppers.

`; +export const simpleHtml = `

Space

`; diff --git a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts index 2bbee2c7968..39ef8e7f536 100644 --- a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts +++ b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts @@ -7,7 +7,9 @@ import { carArticle } from "./html-articles"; describe("Test html-to-lexical parser", () => { it("should parse html string to lexical object", async () => { - const lexicalObject = parseToLexicalObject(carArticle); - expect(lexicalObject).toEqual({ test: "string" }); + parseToLexicalObject(carArticle, data => { + console.log("On success", data); + expect(data).toEqual({ test: "string" }); + }); }); }); diff --git a/packages/html-to-lexical-parser/package.json b/packages/html-to-lexical-parser/package.json index d9a0072cd70..3ea012f7d0b 100644 --- a/packages/html-to-lexical-parser/package.json +++ b/packages/html-to-lexical-parser/package.json @@ -5,7 +5,8 @@ "@lexical/headless": "^0.11.3", "@lexical/html": "^0.11.3", "@webiny/lexical-editor": "0.0.0", - "jsdom": "22.1.0" + "jsdom": "22.1.0", + "lexical": "^0.11.3" }, "devDependencies": { "@types/jsdom": "21.1.3", diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts index 54362eb1170..9f57c0a2a16 100644 --- a/packages/html-to-lexical-parser/src/index.ts +++ b/packages/html-to-lexical-parser/src/index.ts @@ -5,21 +5,20 @@ import { } from "@webiny/lexical-editor"; import { createHeadlessEditor } from "@lexical/headless"; import { $generateNodesFromDOM } from "@lexical/html"; -import { TextEncoder } from "util"; +import { $getRoot, $insertNodes } from "lexical"; const jsdom = require("jsdom"); const { JSDOM } = jsdom; -global.TextEncoder = TextEncoder; - /** * Parse html string to lexical object. * This parser by default uses the Webiny lexical nodes. */ export const parseToLexicalObject = ( htmlString: string, + onSuccess: (data: Record) => void, onError?: (onError: Error) => void -): Record => { +): void => { if (!htmlString?.length) { return JSON.parse(generateInitialLexicalValue()); } @@ -30,9 +29,16 @@ export const parseToLexicalObject = ( theme: getTheme() }); - const dom = new JSDOM(htmlString); - - // Convert to lexical node objects format that can be stored in db. - const nodesData = $generateNodesFromDOM(editor, dom).map(node => node.exportJSON()); - return nodesData; + editor.update(() => { + // Generate dom tree + const dom = new JSDOM(htmlString); + // Convert to lexical node objects format that can be stored in db. + const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document); + // Select the root + $getRoot().select(); + // Insert them at a selection. + $insertNodes(lexicalNodes); + // callback + onSuccess(editor.getEditorState().toJSON()); + }); }; diff --git a/yarn.lock b/yarn.lock index 369265a8c9e..eec2661db0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16190,6 +16190,7 @@ __metadata: "@webiny/lexical-editor": 0.0.0 "@webiny/project-utils": 0.0.0 jsdom: 22.1.0 + lexical: ^0.11.3 languageName: unknown linkType: soft From fc765ea29742e52b5cf2bb446d11f2725c0258be Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Tue, 3 Oct 2023 16:09:40 +0200 Subject: [PATCH 07/31] wip: set discrete flag to update the lexical nodes tree synchronously --- .../__tests__/html-to-lexical.test.ts | 4 +-- packages/html-to-lexical-parser/package.json | 4 +-- packages/html-to-lexical-parser/src/index.ts | 35 ++++++++++++------ yarn.lock | 36 ++++++++++++++----- 4 files changed, 56 insertions(+), 23 deletions(-) diff --git a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts index 39ef8e7f536..5a829f4150b 100644 --- a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts +++ b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts @@ -3,11 +3,11 @@ */ import { parseToLexicalObject } from "~/index"; -import { carArticle } from "./html-articles"; +import { simpleHtml } from "./html-articles"; describe("Test html-to-lexical parser", () => { it("should parse html string to lexical object", async () => { - parseToLexicalObject(carArticle, data => { + parseToLexicalObject(simpleHtml, data => { console.log("On success", data); expect(data).toEqual({ test: "string" }); }); diff --git a/packages/html-to-lexical-parser/package.json b/packages/html-to-lexical-parser/package.json index 3ea012f7d0b..2db5d770427 100644 --- a/packages/html-to-lexical-parser/package.json +++ b/packages/html-to-lexical-parser/package.json @@ -2,8 +2,8 @@ "name": "@webiny/html-to-lexical-parser", "version": "0.0.0", "dependencies": { - "@lexical/headless": "^0.11.3", - "@lexical/html": "^0.11.3", + "@lexical/headless": "^0.12.2", + "@lexical/html": "^0.12.2", "@webiny/lexical-editor": "0.0.0", "jsdom": "22.1.0", "lexical": "^0.11.3" diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts index 9f57c0a2a16..32244369e6b 100644 --- a/packages/html-to-lexical-parser/src/index.ts +++ b/packages/html-to-lexical-parser/src/index.ts @@ -5,7 +5,7 @@ import { } from "@webiny/lexical-editor"; import { createHeadlessEditor } from "@lexical/headless"; import { $generateNodesFromDOM } from "@lexical/html"; -import { $getRoot, $insertNodes } from "lexical"; +import { $getRoot, $getSelection } from "lexical"; const jsdom = require("jsdom"); const { JSDOM } = jsdom; @@ -29,16 +29,29 @@ export const parseToLexicalObject = ( theme: getTheme() }); - editor.update(() => { - // Generate dom tree - const dom = new JSDOM(htmlString); - // Convert to lexical node objects format that can be stored in db. - const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document); - // Select the root - $getRoot().select(); - // Insert them at a selection. - $insertNodes(lexicalNodes); - // callback + editor.update( + async () => { + // Generate dom tree + const dom = new JSDOM(htmlString); + // Convert to lexical node objects format that can be stored in db. + const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document); + + // Select the root + $getRoot().select(); + + // Insert them at a selection. + const selection = $getSelection(); + if (selection) { + selection.insertNodes(lexicalNodes); + } + }, + /** + * discrete – If true, prevents this update from being batched, forcing it to run synchronously. + */ + { discrete: true } + ); + + editor.getEditorState().read(() => { onSuccess(editor.getEditorState().toJSON()); }); }; diff --git a/yarn.lock b/yarn.lock index eec2661db0d..bc2a4217957 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7787,12 +7787,12 @@ __metadata: languageName: node linkType: hard -"@lexical/headless@npm:^0.11.3": - version: 0.11.3 - resolution: "@lexical/headless@npm:0.11.3" +"@lexical/headless@npm:^0.12.2": + version: 0.12.2 + resolution: "@lexical/headless@npm:0.12.2" peerDependencies: - lexical: 0.11.3 - checksum: 9bbabea45714a69deff7a6df7df55033c71904ffcee7398b8713e6178aace28f2fd75e111ca1f4113b13b2161498413ec82d90587f6289b35ab0de355494ebd2 + lexical: 0.12.2 + checksum: 8954bf8efb6cabe6f69e138d65e4785999b6b675275e46937c1573e55376598a153f408904583976543d46fa986da9c6f04c1a34117e2b51768599afa190987e languageName: node linkType: hard @@ -7807,7 +7807,7 @@ __metadata: languageName: node linkType: hard -"@lexical/html@npm:0.11.3, @lexical/html@npm:^0.11.3": +"@lexical/html@npm:0.11.3": version: 0.11.3 resolution: "@lexical/html@npm:0.11.3" dependencies: @@ -7818,6 +7818,17 @@ __metadata: languageName: node linkType: hard +"@lexical/html@npm:^0.12.2": + version: 0.12.2 + resolution: "@lexical/html@npm:0.12.2" + dependencies: + "@lexical/selection": 0.12.2 + peerDependencies: + lexical: 0.12.2 + checksum: a3538d923f2ffd5d0b5db186016d5cdeab21f42c55ac40d393b7cfbd59f1e42f12515b79ce291eafa57d67960995bbe90bc46a5042a177753f4acccfc1746fa1 + languageName: node + linkType: hard + "@lexical/link@npm:0.11.3, @lexical/link@npm:^0.11.3": version: 0.11.3 resolution: "@lexical/link@npm:0.11.3" @@ -7948,6 +7959,15 @@ __metadata: languageName: node linkType: hard +"@lexical/selection@npm:0.12.2": + version: 0.12.2 + resolution: "@lexical/selection@npm:0.12.2" + peerDependencies: + lexical: 0.12.2 + checksum: 4c35dc644c401c3782ed8a10e404a92b1f18c763332035e1f8707a1ac6143754b463bbb122b7cc82c99766e0b5650927025e2d081538ffbbd76386cf9cf4c66c + languageName: node + linkType: hard + "@lexical/table@npm:0.11.3": version: 0.11.3 resolution: "@lexical/table@npm:0.11.3" @@ -16183,8 +16203,8 @@ __metadata: version: 0.0.0-use.local resolution: "@webiny/html-to-lexical-parser@workspace:packages/html-to-lexical-parser" dependencies: - "@lexical/headless": ^0.11.3 - "@lexical/html": ^0.11.3 + "@lexical/headless": ^0.12.2 + "@lexical/html": ^0.12.2 "@types/jsdom": 21.1.3 "@webiny/cli": 0.0.0 "@webiny/lexical-editor": 0.0.0 From 72590d3db6f2cead049cfa75fa7c844df48d7e97 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Tue, 3 Oct 2023 16:13:43 +0200 Subject: [PATCH 08/31] wip: simple html paragraph test --- .../__tests__/html-to-lexical.test.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts index 5a829f4150b..a1d5fa5492b 100644 --- a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts +++ b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts @@ -8,8 +8,36 @@ import { simpleHtml } from "./html-articles"; describe("Test html-to-lexical parser", () => { it("should parse html string to lexical object", async () => { parseToLexicalObject(simpleHtml, data => { - console.log("On success", data); - expect(data).toEqual({ test: "string" }); + expect(data).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "Space", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + styles: [], + type: "paragraph-element", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); }); }); }); From d1704396392175cc0e347aa6c87ccac987affbbd Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 5 Oct 2023 11:53:51 +0200 Subject: [PATCH 09/31] wip: removed theme from the init ocnfig --- packages/html-to-lexical-parser/src/index.ts | 13 ++++--------- .../src/components/Editor/RichTextEditor.tsx | 4 ++-- .../src/components/LexicalHtmlRenderer.tsx | 4 ++-- packages/lexical-editor/src/index.tsx | 1 + .../src/nodes/{webinyNodes.ts => allNodes.ts} | 2 +- .../src/utils/getSupportedNodeList.ts | 4 ++-- 6 files changed, 12 insertions(+), 16 deletions(-) rename packages/lexical-editor/src/nodes/{webinyNodes.ts => allNodes.ts} (98%) diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts index 32244369e6b..5d0355ef5b6 100644 --- a/packages/html-to-lexical-parser/src/index.ts +++ b/packages/html-to-lexical-parser/src/index.ts @@ -1,11 +1,7 @@ -import { - generateInitialLexicalValue, - getSupportedNodeList, - getTheme -} from "@webiny/lexical-editor"; import { createHeadlessEditor } from "@lexical/headless"; import { $generateNodesFromDOM } from "@lexical/html"; import { $getRoot, $getSelection } from "lexical"; +import { allNodes } from "@webiny/lexical-editor/nodes/allNodes"; const jsdom = require("jsdom"); const { JSDOM } = jsdom; @@ -20,13 +16,12 @@ export const parseToLexicalObject = ( onError?: (onError: Error) => void ): void => { if (!htmlString?.length) { - return JSON.parse(generateInitialLexicalValue()); + return; } const editor = createHeadlessEditor({ - nodes: getSupportedNodeList(), - onError: onError, - theme: getTheme() + nodes: allNodes, + onError: onError }); editor.update( diff --git a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx index caa07b460b3..539b1b8a34b 100644 --- a/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx +++ b/packages/lexical-editor/src/components/Editor/RichTextEditor.tsx @@ -19,7 +19,7 @@ import { LexicalValue, ThemeEmotionMap, ToolbarActionPlugin } from "~/types"; import { Placeholder } from "~/ui/Placeholder"; import { generateInitialLexicalValue } from "~/utils/generateInitialLexicalValue"; import { webinyEditorTheme, WebinyTheme } from "~/themes/webinyLexicalTheme"; -import { WebinyNodes } from "~/nodes/webinyNodes"; +import { allNodes } from "~/nodes/allNodes"; import { SharedHistoryContext, useSharedHistoryContext } from "~/context/SharedHistoryContext"; import { useRichTextEditor } from "~/hooks/useRichTextEditor"; import { toTypographyEmotionMap } from "~/utils/toTypographyEmotionMap"; @@ -132,7 +132,7 @@ const BaseRichTextEditor: React.FC = ({ onError: (error: Error) => { throw error; }, - nodes: [...WebinyNodes, ...configNodes, ...(nodes || [])], + nodes: [...allNodes, ...configNodes, ...(nodes || [])], theme: { ...webinyEditorTheme, emotionMap: themeEmotionMap } }; diff --git a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx index 156648b1a4b..f6f7d778571 100644 --- a/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx +++ b/packages/lexical-editor/src/components/LexicalHtmlRenderer.tsx @@ -8,7 +8,7 @@ import { ContentEditable } from "@lexical/react/LexicalContentEditable"; import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary"; import { LexicalUpdateStatePlugin } from "~/plugins/LexicalUpdateStatePlugin"; import { Klass, LexicalNode } from "lexical"; -import { WebinyNodes } from "~/nodes/webinyNodes"; +import { allNodes } from "~/nodes/allNodes"; import { webinyEditorTheme, WebinyTheme } from "~/themes/webinyLexicalTheme"; import { ClassNames, CSSObject } from "@emotion/react"; import { toTypographyEmotionMap } from "~/utils/toTypographyEmotionMap"; @@ -37,7 +37,7 @@ export const BaseLexicalHtmlRenderer: React.FC = ({ throw error; }, editable: false, - nodes: [...WebinyNodes, ...(nodes || [])], + nodes: [...allNodes, ...(nodes || [])], theme: { ...webinyEditorTheme, emotionMap: themeEmotionMap, styles: theme.styles } }; diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index 550958fa13e..5eb3df10d8e 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -51,6 +51,7 @@ export { getTheme } from "~/utils/getTheme"; export { INSERT_IMAGE_COMMAND } from "~/commands/insertFiles"; // types export * as types from "./types"; +export * from "./nodes/allNodes"; // config export { LexicalEditorConfig, diff --git a/packages/lexical-editor/src/nodes/webinyNodes.ts b/packages/lexical-editor/src/nodes/allNodes.ts similarity index 98% rename from packages/lexical-editor/src/nodes/webinyNodes.ts rename to packages/lexical-editor/src/nodes/allNodes.ts index f4f004e1951..bd424c89f90 100644 --- a/packages/lexical-editor/src/nodes/webinyNodes.ts +++ b/packages/lexical-editor/src/nodes/allNodes.ts @@ -17,7 +17,7 @@ import { ImageNode } from "~/nodes/ImageNode"; import { LinkNode } from "~/nodes/link-node"; // This is a list of all the nodes that our Lexical implementation supports OOTB. -export const WebinyNodes: ReadonlyArray< +export const allNodes: ReadonlyArray< | Klass | { replace: Klass; diff --git a/packages/lexical-editor/src/utils/getSupportedNodeList.ts b/packages/lexical-editor/src/utils/getSupportedNodeList.ts index 33ecf23755e..ecc7e516161 100644 --- a/packages/lexical-editor/src/utils/getSupportedNodeList.ts +++ b/packages/lexical-editor/src/utils/getSupportedNodeList.ts @@ -1,5 +1,5 @@ import { Klass, LexicalNode } from "lexical"; -import { WebinyNodes } from "~/nodes/webinyNodes"; +import { allNodes } from "~/nodes/allNodes"; /** * Get the supported list of lexical nodes types. @@ -12,5 +12,5 @@ export const getSupportedNodeList = (): ReadonlyArray< with: (node: InstanceType) => LexicalNode; } > => { - return [...WebinyNodes]; + return [...allNodes]; }; From 7e8d3bd6a6d13b038f42947c79043c9fee0b3e9e Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 5 Oct 2023 12:18:10 +0200 Subject: [PATCH 10/31] wip: correct lexical version --- packages/html-to-lexical-parser/package.json | 4 +-- yarn.lock | 36 +++++--------------- 2 files changed, 10 insertions(+), 30 deletions(-) diff --git a/packages/html-to-lexical-parser/package.json b/packages/html-to-lexical-parser/package.json index 2db5d770427..3ea012f7d0b 100644 --- a/packages/html-to-lexical-parser/package.json +++ b/packages/html-to-lexical-parser/package.json @@ -2,8 +2,8 @@ "name": "@webiny/html-to-lexical-parser", "version": "0.0.0", "dependencies": { - "@lexical/headless": "^0.12.2", - "@lexical/html": "^0.12.2", + "@lexical/headless": "^0.11.3", + "@lexical/html": "^0.11.3", "@webiny/lexical-editor": "0.0.0", "jsdom": "22.1.0", "lexical": "^0.11.3" diff --git a/yarn.lock b/yarn.lock index bc2a4217957..eec2661db0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7787,12 +7787,12 @@ __metadata: languageName: node linkType: hard -"@lexical/headless@npm:^0.12.2": - version: 0.12.2 - resolution: "@lexical/headless@npm:0.12.2" +"@lexical/headless@npm:^0.11.3": + version: 0.11.3 + resolution: "@lexical/headless@npm:0.11.3" peerDependencies: - lexical: 0.12.2 - checksum: 8954bf8efb6cabe6f69e138d65e4785999b6b675275e46937c1573e55376598a153f408904583976543d46fa986da9c6f04c1a34117e2b51768599afa190987e + lexical: 0.11.3 + checksum: 9bbabea45714a69deff7a6df7df55033c71904ffcee7398b8713e6178aace28f2fd75e111ca1f4113b13b2161498413ec82d90587f6289b35ab0de355494ebd2 languageName: node linkType: hard @@ -7807,7 +7807,7 @@ __metadata: languageName: node linkType: hard -"@lexical/html@npm:0.11.3": +"@lexical/html@npm:0.11.3, @lexical/html@npm:^0.11.3": version: 0.11.3 resolution: "@lexical/html@npm:0.11.3" dependencies: @@ -7818,17 +7818,6 @@ __metadata: languageName: node linkType: hard -"@lexical/html@npm:^0.12.2": - version: 0.12.2 - resolution: "@lexical/html@npm:0.12.2" - dependencies: - "@lexical/selection": 0.12.2 - peerDependencies: - lexical: 0.12.2 - checksum: a3538d923f2ffd5d0b5db186016d5cdeab21f42c55ac40d393b7cfbd59f1e42f12515b79ce291eafa57d67960995bbe90bc46a5042a177753f4acccfc1746fa1 - languageName: node - linkType: hard - "@lexical/link@npm:0.11.3, @lexical/link@npm:^0.11.3": version: 0.11.3 resolution: "@lexical/link@npm:0.11.3" @@ -7959,15 +7948,6 @@ __metadata: languageName: node linkType: hard -"@lexical/selection@npm:0.12.2": - version: 0.12.2 - resolution: "@lexical/selection@npm:0.12.2" - peerDependencies: - lexical: 0.12.2 - checksum: 4c35dc644c401c3782ed8a10e404a92b1f18c763332035e1f8707a1ac6143754b463bbb122b7cc82c99766e0b5650927025e2d081538ffbbd76386cf9cf4c66c - languageName: node - linkType: hard - "@lexical/table@npm:0.11.3": version: 0.11.3 resolution: "@lexical/table@npm:0.11.3" @@ -16203,8 +16183,8 @@ __metadata: version: 0.0.0-use.local resolution: "@webiny/html-to-lexical-parser@workspace:packages/html-to-lexical-parser" dependencies: - "@lexical/headless": ^0.12.2 - "@lexical/html": ^0.12.2 + "@lexical/headless": ^0.11.3 + "@lexical/html": ^0.11.3 "@types/jsdom": 21.1.3 "@webiny/cli": 0.0.0 "@webiny/lexical-editor": 0.0.0 From 2ed956fcd44fc44f77fa2f34c046634105f02203 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 5 Oct 2023 17:29:31 +0200 Subject: [PATCH 11/31] fix: list item node - insert after --- .../__tests__/html-to-lexical.test.ts | 1 - .../lexical-editor/src/nodes/ListItemNode.ts | 19 +++++-------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts index a1d5fa5492b..e52c6dc9f6f 100644 --- a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts +++ b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts @@ -1,7 +1,6 @@ /** * @jest-environment jsdom */ - import { parseToLexicalObject } from "~/index"; import { simpleHtml } from "./html-articles"; diff --git a/packages/lexical-editor/src/nodes/ListItemNode.ts b/packages/lexical-editor/src/nodes/ListItemNode.ts index e4eebc311c7..84be2ef9738 100644 --- a/packages/lexical-editor/src/nodes/ListItemNode.ts +++ b/packages/lexical-editor/src/nodes/ListItemNode.ts @@ -12,9 +12,9 @@ import { NodeSelection, ParagraphNode, RangeSelection, - SerializedElementNode + SerializedElementNode, + Spread } from "lexical"; -import { Spread } from "lexical"; import { $createListNode, $isListNode, ListNode } from "~/nodes/ListNode"; import { addClassNamesToElement, removeClassNamesFromElement } from "@lexical/utils"; import { @@ -60,9 +60,7 @@ export class ListItemNode extends ElementNode { override createDOM(config: EditorConfig): HTMLElement { const element = document.createElement("li"); const parent = this.getParent(); - - if ($isListNode(parent)) { - updateChildrenListItemValue(parent); + if ($isListNode(parent) && parent.getListType() === "check") { updateListItemChecked(element, this, null, parent); } element.value = this.__value; @@ -73,14 +71,11 @@ export class ListItemNode extends ElementNode { override updateDOM(prevNode: ListItemNode, dom: HTMLElement, config: EditorConfig): boolean { const parent = this.getParent(); - - if ($isListNode(parent)) { - updateChildrenListItemValue(parent); + if ($isListNode(parent) && parent.getListType() === "check") { updateListItemChecked(dom, this, prevNode, parent); } // @ts-expect-error - this is always HTMLListItemElement dom.value = this.__value; - $setListItemThemeClassNames(dom, config.theme, this); return false; @@ -181,7 +176,7 @@ export class ListItemNode extends ElementNode { const afterListNode = node.getParentOrThrow(); if ($isListNode(afterListNode)) { - afterListNode; + updateChildrenListItemValue(afterListNode); } return after; @@ -306,10 +301,6 @@ export class ListItemNode extends ElementNode { self.__checked = checked; } - toggleChecked(): void { - this.setChecked(!this.__checked); - } - override getIndent(): number { // If we don't have a parent, we are likely serializing const parent = this.getParent(); From 4c0195742945ff8b140da1dbba58c43cc9880b8d Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 5 Oct 2023 22:19:20 +0200 Subject: [PATCH 12/31] wip: handled problem where text node don't have parent element --- packages/html-to-lexical-parser/README.md | 9 +- .../__tests__/html-articles.ts | 297 ++++++++++++++++++ .../__tests__/volvo-article.html | 296 +++++++++++++++++ packages/html-to-lexical-parser/src/index.ts | 21 +- packages/lexical-editor/src/index.tsx | 2 - .../src/utils/getSupportedNodeList.ts | 16 - packages/lexical-editor/src/utils/getTheme.ts | 9 - 7 files changed, 617 insertions(+), 33 deletions(-) create mode 100644 packages/html-to-lexical-parser/__tests__/volvo-article.html delete mode 100644 packages/lexical-editor/src/utils/getSupportedNodeList.ts delete mode 100644 packages/lexical-editor/src/utils/getTheme.ts diff --git a/packages/html-to-lexical-parser/README.md b/packages/html-to-lexical-parser/README.md index 623f87fe26d..ec1b4457652 100644 --- a/packages/html-to-lexical-parser/README.md +++ b/packages/html-to-lexical-parser/README.md @@ -1,15 +1,14 @@ -# @webiny/lexical-editor-actions -[![](https://img.shields.io/npm/dw/@webiny/lexical-editor-actions.svg)](https://www.npmjs.com/package/@webiny/lexical-editor) -[![](https://img.shields.io/npm/v/@webiny/lexical-editor-actions.svg)](https://www.npmjs.com/package/@webiny/lexical-editor) +# @webiny/lexical-html-to-lexical-parser + +[![](https://img.shields.io/npm/dw/@webiny/lexical-html-to-lexical-parser.svg)](https://www.npmjs.com/package/@webiny/llexical-html-to-lexical-parser) +[![](https://img.shields.io/npm/v/@webiny/lexical-html-to-lexical-parser.svg)](https://www.npmjs.com/package/@webiny/lexical-html-to-lexical-parser) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) - ## About This package provides actions plugins for Lexical editor. - ## Where is it used? Currently, this packaged is used in [@webiny/app-serverless-cms](../app-serverless-cms). diff --git a/packages/html-to-lexical-parser/__tests__/html-articles.ts b/packages/html-to-lexical-parser/__tests__/html-articles.ts index 3466a756535..f0bee7598d4 100644 --- a/packages/html-to-lexical-parser/__tests__/html-articles.ts +++ b/packages/html-to-lexical-parser/__tests__/html-articles.ts @@ -1,2 +1,299 @@ export const carArticle = `

2024 Lexus TX Enters 3-Row SUV Price Brawl With Competitive MSRP

The all-new Lexus luxury SUV starts at a reasonable price for the segment.

Related Video

Lexus's first true midsize three-row SUV, the all-new TX, is almost here. With a properly boxy body atop car-based underpinnings, it's a far roomier offering than the half-baked, extended version of the last-generation two-row RX Lexus sold previously. It's also far more family friendly than the brand's body-on-frame, more-off-road-oriented GX and LX SUVs. Those may have had three rows of seating, but were aimed at a different swath of buyers than those shopping for mall crawlers like the Acura MDX, Audi Q7, Infiniti QX60, Volvo XC90, and so on. With the TX set to launch soon, we finally know how much it'll cost.

2024 Lexus TX Price? It's Nice

There will be several versions of the TX available at launch, with a third option coming later. The entry-level TX350 comes powered by a 275-hp turbocharged 2.4-liter four-cylinder engine; the TX500h F Sport Performance model uses a hybridized version of that 2.4-liter engine and produces a saucier 366 total hp; and coming later there is a TX550h+ plug-in hybrid that combines several electric motors with a 3.5-liter V-6 and delivers 406 hp.

Each of these TX variants is subdivided into narrower trim levels, with the TX350 available in base, Premium, and Luxury specifications, with each offered with front- or all-wheel-drive for an extra $1,600; the TX500h is only available with all-wheel-drive and in Premium and Luxury trims. TX550h+ details are forthcoming.

Lexus TX350 Pricing and Features

  • TX350: The least-expensive TX350, with front-wheel-drive and no options, starts at $55,050 including Lexus's rich $1,350 destination charge. Seven-passenger seating is standard, with eight-way power adjustment and heating on the fronts, and the cabin is slathered in Lexus's NuLuxe faux-leather. Proximity key access is included, as is a power-opening tailgate with hands-free sensor, manual rear side window sunshades, a wireless phone charger, 14-inch touchscreen, wireless Apple CarPlay and Android Auto, rain-sensing wipers, and Lexus's Safety System+ 3.0 active safety gear (adaptive cruise control, automated emergency braking, lane-keep assist, blind-spot monitoring, and auto high beams).
  • TX350 Premium: Besides opening access to a number of higher-end option packages, the Premium trim adds as standard unique 20-inch wheels, a panoramic sunroof, power-folding and reclining third-row seats, and front-seat ventilation and memory function. The price rises to $58,450, with AWD again a $1,600 option.
  • TX350 Luxury: The biggest changeover you'll see going from a TX350 Premium to Luxury model is leather. It replaces the faux stuff in the lesser 350s. Additional standard content includes "thematic ambient lighting," 10-way power adjustment for the front seats, heated outboard rear seats, a heated steering wheel, and headlight washers. If you want six-passenger seating with second-row captain's chairs, you'll need to upgrade to this Luxury spec, by the way. The price is $60,950, plus $1,600 for AWD.

TX500h F Sport Performance Pricing and Features

  • TX500h Premium: The TX500h comes standard with all-wheel drive and six-passenger seating, presumably in keeping with its zestier vibe relative to the other TXs, albeit slathered in NuLuxe faux leather. (You can't get a 500h with seven-passenger seating, in fact.) Lexus sprinkles a bunch of F Sport bits, from sport pedals to the steering wheel to the dark gray 22-inch wheels. It is otherwise equipped like the TX350 Luxury, plus heated and ventilated second-row seats, and starts at $69,350.
  • TX500h Luxury: For actual leather, you'll want the $72,650 TX500h Luxury, which replaces NuLuxe with real stuff. It also includes a heated steering wheel, panoramic sunroof, 21-speaker Mark Levinson audio system, and unique 22-inch wheels.

Option packages include the Technology package (12.3-inch gauge cluster screen, 360-degree camera, self-parking, digital rearview mirror, and head-up display, depending on the trim it's added to) for between $1,050 and $2,380; the $895 Convenience package (digital key, front cross-traffic alert, traffic jam assist); and $250 Cold Area package (windshield wiper de-icer, heated wheel on models not equipped).

Compared to the Acura MDX, Audi Q7, and Infiniti QX60, the TX is priced in the thick of things. The Acura starts at $51,045, the Infiniti at $50,845, and the Audi is way out there at $60,695. That puts the TX right in the mix as a compelling option for shoppers.

`; export const simpleHtml = `

Space

`; + +export const volvoArticle = `
+

Volvo’s Cheapest, Quickest New Vehicle Also Is + All-Electric

The entry-level 2025 Volvo EX30 is genuinely affordable—EV or not.

+
+
+
+
+
+ +
+
+
+ + +
+ +
+

Of all the cool things we + learned about the 2025 Volvo EX30 when we got our first look in June, its + totally reasonable $36,000 entry point was the headliner. Full pricing, now revealed, + confirms that the upcoming EX30 stands as Volvo's cheapest and fastest-accelerating + vehicle—albeit its smallest—and it's electric.

+
+
+ +
+
+

What Is the Volvo EX30?

+

The brand-new 2025 Volvo + EX30 is an all-electric sub-compact SUV. It's Volvo's smallest-ever SUV (it's the length + of a Jeep Renegade!) and Volvo's least expensive electric SUV (and least-expensive + vehicle, period) in the U.S. In terms of looks, it kind of resembles a baby version of + the EX90 electric three-row SUV, although it's built on a new platform called SEA + (Scalable Electric Architecture or Sustainable Experience Architecture) developed with + Chinese parent company Geely rather than the Volvo-designed SPA2 platform of the EX90. + Just for funsies, it'll be offered in Moss Yellow, Cloud Blue, Vapour Grey, Crystal + White, or Onyx Black exterior colors, with Mist, Pine, Indigo, and Breeze interiors.

+
+

Why Is the EX30 So Cheap?

+
+

Considering Volvo's EX30 + gets hit with the 25 percent tariff (and probably won't qualify for U.S. federal tax + credits) because it's built in China, it's completely necessary that this luxury-branded + electric SUV starts in the mid-$30,000s to stay competitive. It's built in China on a Geely-based + platform, which means cheaper labor and a shared platform, contributing to a + cheaper price tag. Furthermore, a rather symmetrical interior (for easier crossover + between right- and left-hand drive markets) and a concerted effort to keep parts at a + minimum (think sparsity of interior buttons and switches) also keep costs down while + shrinking its environmental footprint. These are the kinds of techniques employed by, + you guessed it, Tesla, on that automaker's less expensive Model 3 and Model Y. And most + obvious of all, the EX30 is physically exceedingly small, essentially a subcompact SUV, + a generally advantageous trait when it comes to price.

+

Single Motor Extended Range EX30: + $36,245

+

The first EX30 powertrain, + the Single Motor Extended Range, consists of one motor delivering 268 hp and 253 lb-ft + of torque to the rear wheels. It should accelerate to 60 mph in 5.1 seconds, although we + haven't tested one yet to confirm. Range comes in at 275 miles.

+

The Single Motor Extended + Range comes in Core, Plus, and Ultra equipment levels (trims). The base Core ($36,245) + comes with a large 12.3-inch tablet-style central display screen with Google built-in + (Google Maps and Google Assistant), wireless Apple CarPlay, and a bevy of advanced + safety features (including a driver alert system, blind spot information system, and + front and rear collision mitigation).

+

Upgrading to Plus ($40,195) + affords a premium Harman Kardon sound system, a dual top panoramic roof in Onyx Black, + and 19-inch wheels.

+

The Ultra ($41,895) tacks on + the next generation of Pilot Assist with Lane Change Assistance, a 360-degree camera + with a 3D view, and Park Pilot Assist.

+
+
+
+
+
+
+
+
+
+

Twin Motor Performance EX30: + $46,195

+

The second EX30 powertrain, + the Twin Motor Performance, is a 422-hp (154-hp front, 268 hp rear), 400 lb-ft two-motor + all-wheel-drive performance iteration of the tiny-but-mighty SUV. With a 0-60 time of + 3.4 seconds, it's hailed as Volvo's quickest car ever and should rival the larger, + 576-hp Kia EV6 GT. Range is expected to be 260 miles. Both EX30 powertrains are fed by a + 69-kWh Li-NMC battery that can charge from 10 to 80 percent in about 27 minutes with a + peak charging rate of 153 kW on the Twin Motor Performance.

+

The Twin Motor Performance + comes in Plus ($46,195) and Ultra ($47,895). Simple math tells us that extra motor comes + at a $6,000 premium.

+

Expect deliveries of the + Volvo EX30 to begin in the summer of 2024.

+

How Much Is the 2025 Volvo EX30? +

+

Single Motor + Extended Range

+
+
+
    +
  • Core - $36,245
  • +
  • Plus - $40,195
  • +
  • Ultra - $41,895
  • +
+
+
+

Twin Motor + Performance

+
+
+
    +
  • Plus - $46,195
  • +
  • Ultra - $47,895
  • +
+
+
+

+

How Much Is the Volvo EX30 Cross + Country?

+

Pricing is still forthcoming + for + the 2025 Volvo EX30 Cross Country, + which adds ground clearance, skidplates, 19-inch wheels (18s optional), and aggressive + black plastic cladding. The Cross Country should be available for order next year.

+
+
+
+ + +
+ +
+ + +
+
`; diff --git a/packages/html-to-lexical-parser/__tests__/volvo-article.html b/packages/html-to-lexical-parser/__tests__/volvo-article.html new file mode 100644 index 00000000000..3b1745ebcb9 --- /dev/null +++ b/packages/html-to-lexical-parser/__tests__/volvo-article.html @@ -0,0 +1,296 @@ +
+

Volvo’s Cheapest, Quickest New Vehicle Also Is + All-Electric

The entry-level 2025 Volvo EX30 is genuinely affordable—EV or not.

+
+
+
+
+
+ +
+
+
+ + +
+ +
+

Of all the cool things we + learned about the 2025 Volvo EX30 when we got our first look in June, its + totally reasonable $36,000 entry point was the headliner. Full pricing, now revealed, + confirms that the upcoming EX30 stands as Volvo's cheapest and fastest-accelerating + vehicle—albeit its smallest—and it's electric.

+
+
+ +
+
+

What Is the Volvo EX30?

+

The brand-new 2025 Volvo + EX30 is an all-electric sub-compact SUV. It's Volvo's smallest-ever SUV (it's the length + of a Jeep Renegade!) and Volvo's least expensive electric SUV (and least-expensive + vehicle, period) in the U.S. In terms of looks, it kind of resembles a baby version of + the EX90 electric three-row SUV, although it's built on a new platform called SEA + (Scalable Electric Architecture or Sustainable Experience Architecture) developed with + Chinese parent company Geely rather than the Volvo-designed SPA2 platform of the EX90. + Just for funsies, it'll be offered in Moss Yellow, Cloud Blue, Vapour Grey, Crystal + White, or Onyx Black exterior colors, with Mist, Pine, Indigo, and Breeze interiors.

+
+

Why Is the EX30 So Cheap?

+
+

Considering Volvo's EX30 + gets hit with the 25 percent tariff (and probably won't qualify for U.S. federal tax + credits) because it's built in China, it's completely necessary that this luxury-branded + electric SUV starts in the mid-$30,000s to stay competitive. It's built in China on a Geely-based + platform, which means cheaper labor and a shared platform, contributing to a + cheaper price tag. Furthermore, a rather symmetrical interior (for easier crossover + between right- and left-hand drive markets) and a concerted effort to keep parts at a + minimum (think sparsity of interior buttons and switches) also keep costs down while + shrinking its environmental footprint. These are the kinds of techniques employed by, + you guessed it, Tesla, on that automaker's less expensive Model 3 and Model Y. And most + obvious of all, the EX30 is physically exceedingly small, essentially a subcompact SUV, + a generally advantageous trait when it comes to price.

+

Single Motor Extended Range EX30: + $36,245

+

The first EX30 powertrain, + the Single Motor Extended Range, consists of one motor delivering 268 hp and 253 lb-ft + of torque to the rear wheels. It should accelerate to 60 mph in 5.1 seconds, although we + haven't tested one yet to confirm. Range comes in at 275 miles.

+

The Single Motor Extended + Range comes in Core, Plus, and Ultra equipment levels (trims). The base Core ($36,245) + comes with a large 12.3-inch tablet-style central display screen with Google built-in + (Google Maps and Google Assistant), wireless Apple CarPlay, and a bevy of advanced + safety features (including a driver alert system, blind spot information system, and + front and rear collision mitigation).

+

Upgrading to Plus ($40,195) + affords a premium Harman Kardon sound system, a dual top panoramic roof in Onyx Black, + and 19-inch wheels.

+

The Ultra ($41,895) tacks on + the next generation of Pilot Assist with Lane Change Assistance, a 360-degree camera + with a 3D view, and Park Pilot Assist.

+
+
+
+
+
+
+
+
+
+

Twin Motor Performance EX30: + $46,195

+

The second EX30 powertrain, + the Twin Motor Performance, is a 422-hp (154-hp front, 268 hp rear), 400 lb-ft two-motor + all-wheel-drive performance iteration of the tiny-but-mighty SUV. With a 0-60 time of + 3.4 seconds, it's hailed as Volvo's quickest car ever and should rival the larger, + 576-hp Kia EV6 GT. Range is expected to be 260 miles. Both EX30 powertrains are fed by a + 69-kWh Li-NMC battery that can charge from 10 to 80 percent in about 27 minutes with a + peak charging rate of 153 kW on the Twin Motor Performance.

+

The Twin Motor Performance + comes in Plus ($46,195) and Ultra ($47,895). Simple math tells us that extra motor comes + at a $6,000 premium.

+

Expect deliveries of the + Volvo EX30 to begin in the summer of 2024.

+

How Much Is the 2025 Volvo EX30? +

+

Single Motor + Extended Range

+
+
+
    +
  • Core - $36,245
  • +
  • Plus - $40,195
  • +
  • Ultra - $41,895
  • +
+
+
+

Twin Motor + Performance

+
+
+
    +
  • Plus - $46,195
  • +
  • Ultra - $47,895
  • +
+
+
+

+

How Much Is the Volvo EX30 Cross + Country?

+

Pricing is still forthcoming + for + the 2025 Volvo EX30 Cross Country, + which adds ground clearance, skidplates, 19-inch wheels (18s optional), and aggressive + black plastic cladding. The Cross Country should be available for order next year.

+
+
+
+ + +
+ +
+ + +
+
diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts index 5d0355ef5b6..b174dc753e1 100644 --- a/packages/html-to-lexical-parser/src/index.ts +++ b/packages/html-to-lexical-parser/src/index.ts @@ -2,6 +2,7 @@ import { createHeadlessEditor } from "@lexical/headless"; import { $generateNodesFromDOM } from "@lexical/html"; import { $getRoot, $getSelection } from "lexical"; import { allNodes } from "@webiny/lexical-editor/nodes/allNodes"; +import { $createParagraphNode } from "@webiny/lexical-editor/nodes/ParagraphNode"; const jsdom = require("jsdom"); const { JSDOM } = jsdom; @@ -29,7 +30,25 @@ export const parseToLexicalObject = ( // Generate dom tree const dom = new JSDOM(htmlString); // Convert to lexical node objects format that can be stored in db. - const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document); + const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document).map(node => { + /** + * Text node alone without parent element node(like paragraph) is not valid node. In that case + * lexical will throw an error. + * In the code below, to fix this issue, we append the text node inside the paragraph node(parent element). + * + * Case when text node don't have parent element: + * + * When we parse the DOM, sometimes, 'span' html tags don't have parent elements that match the + * lexical node elements, like paragraph or headings. In that case lexical will parse the span + * as text node, but without parent element. + */ + if (node.getType() === "text" && node.getParent() === null) { + const paragraphNode = $createParagraphNode(); + paragraphNode.append(node); + return paragraphNode; + } + return node; + }); // Select the root $getRoot().select(); diff --git a/packages/lexical-editor/src/index.tsx b/packages/lexical-editor/src/index.tsx index 5eb3df10d8e..ee5e7fe5cc0 100644 --- a/packages/lexical-editor/src/index.tsx +++ b/packages/lexical-editor/src/index.tsx @@ -45,8 +45,6 @@ export { ImagesPlugin } from "~/plugins/ImagesPlugin/ImagesPlugin"; export { generateInitialLexicalValue } from "~/utils/generateInitialLexicalValue"; export { isValidLexicalData } from "~/utils/isValidLexicalData"; export { clearNodeFormatting } from "~/utils/nodes/clearNodeFormating"; -export { getSupportedNodeList } from "~/utils/getSupportedNodeList"; -export { getTheme } from "~/utils/getTheme"; // Commands export { INSERT_IMAGE_COMMAND } from "~/commands/insertFiles"; // types diff --git a/packages/lexical-editor/src/utils/getSupportedNodeList.ts b/packages/lexical-editor/src/utils/getSupportedNodeList.ts deleted file mode 100644 index ecc7e516161..00000000000 --- a/packages/lexical-editor/src/utils/getSupportedNodeList.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Klass, LexicalNode } from "lexical"; -import { allNodes } from "~/nodes/allNodes"; - -/** - * Get the supported list of lexical nodes types. - * These nodes are initialized in the lexical editor, for the page builder and headless CMS apps. - */ -export const getSupportedNodeList = (): ReadonlyArray< - | Klass - | { - replace: Klass; - with: (node: InstanceType) => LexicalNode; - } -> => { - return [...allNodes]; -}; diff --git a/packages/lexical-editor/src/utils/getTheme.ts b/packages/lexical-editor/src/utils/getTheme.ts deleted file mode 100644 index 2d6d33334a1..00000000000 --- a/packages/lexical-editor/src/utils/getTheme.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { WebinyEditorTheme, webinyEditorTheme } from "~/themes/webinyLexicalTheme"; -import { EditorThemeClasses } from "lexical"; - -/** - * Get webiny theme used for lexical editor - */ -export const getTheme = (): EditorThemeClasses | WebinyEditorTheme => { - return webinyEditorTheme; -}; From c2d8fdb00ea569410b7b8ff2db513767f39dd301 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Fri, 6 Oct 2023 10:00:23 +0200 Subject: [PATCH 13/31] wip: add configuration for custom nodes, update readme --- packages/html-to-lexical-parser/README.md | 28 +- .../__tests__/html-articles.ts | 2 + .../__tests__/html-to-lexical.test.ts | 4 +- .../__tests__/volvo-article.html | 296 ------------------ packages/html-to-lexical-parser/src/index.ts | 25 +- packages/html-to-lexical-parser/src/types.ts | 11 + .../lexical-editor/src/nodes/ImageNode.tsx | 13 +- 7 files changed, 66 insertions(+), 313 deletions(-) delete mode 100644 packages/html-to-lexical-parser/__tests__/volvo-article.html create mode 100644 packages/html-to-lexical-parser/src/types.ts diff --git a/packages/html-to-lexical-parser/README.md b/packages/html-to-lexical-parser/README.md index ec1b4457652..89c5874e20d 100644 --- a/packages/html-to-lexical-parser/README.md +++ b/packages/html-to-lexical-parser/README.md @@ -7,8 +7,30 @@ ## About -This package provides actions plugins for Lexical editor. +This package provides method for parsing html to lexical. + +## Usage + +To parse the html string, you need to import `parseHtmlToLexical` function, and provide +the html string. + +```tsx +import {parseHtmlToLexical} from "@webiny/lexical-html-to-lexical-parser"; + +const html = "

My paragraph

"; + +parseHtmlToLexical(html, data => { + // success + console.log("data is parsed") + }, + error => { + // error + console.log("error", error.message); + }); +``` + + + + -## Where is it used? -Currently, this packaged is used in [@webiny/app-serverless-cms](../app-serverless-cms). diff --git a/packages/html-to-lexical-parser/__tests__/html-articles.ts b/packages/html-to-lexical-parser/__tests__/html-articles.ts index f0bee7598d4..73a6449719f 100644 --- a/packages/html-to-lexical-parser/__tests__/html-articles.ts +++ b/packages/html-to-lexical-parser/__tests__/html-articles.ts @@ -1,6 +1,8 @@ export const carArticle = `

2024 Lexus TX Enters 3-Row SUV Price Brawl With Competitive MSRP

The all-new Lexus luxury SUV starts at a reasonable price for the segment.

Related Video

Lexus's first true midsize three-row SUV, the all-new TX, is almost here. With a properly boxy body atop car-based underpinnings, it's a far roomier offering than the half-baked, extended version of the last-generation two-row RX Lexus sold previously. It's also far more family friendly than the brand's body-on-frame, more-off-road-oriented GX and LX SUVs. Those may have had three rows of seating, but were aimed at a different swath of buyers than those shopping for mall crawlers like the Acura MDX, Audi Q7, Infiniti QX60, Volvo XC90, and so on. With the TX set to launch soon, we finally know how much it'll cost.

2024 Lexus TX Price? It's Nice

There will be several versions of the TX available at launch, with a third option coming later. The entry-level TX350 comes powered by a 275-hp turbocharged 2.4-liter four-cylinder engine; the TX500h F Sport Performance model uses a hybridized version of that 2.4-liter engine and produces a saucier 366 total hp; and coming later there is a TX550h+ plug-in hybrid that combines several electric motors with a 3.5-liter V-6 and delivers 406 hp.

Each of these TX variants is subdivided into narrower trim levels, with the TX350 available in base, Premium, and Luxury specifications, with each offered with front- or all-wheel-drive for an extra $1,600; the TX500h is only available with all-wheel-drive and in Premium and Luxury trims. TX550h+ details are forthcoming.

Lexus TX350 Pricing and Features

  • TX350: The least-expensive TX350, with front-wheel-drive and no options, starts at $55,050 including Lexus's rich $1,350 destination charge. Seven-passenger seating is standard, with eight-way power adjustment and heating on the fronts, and the cabin is slathered in Lexus's NuLuxe faux-leather. Proximity key access is included, as is a power-opening tailgate with hands-free sensor, manual rear side window sunshades, a wireless phone charger, 14-inch touchscreen, wireless Apple CarPlay and Android Auto, rain-sensing wipers, and Lexus's Safety System+ 3.0 active safety gear (adaptive cruise control, automated emergency braking, lane-keep assist, blind-spot monitoring, and auto high beams).
  • TX350 Premium: Besides opening access to a number of higher-end option packages, the Premium trim adds as standard unique 20-inch wheels, a panoramic sunroof, power-folding and reclining third-row seats, and front-seat ventilation and memory function. The price rises to $58,450, with AWD again a $1,600 option.
  • TX350 Luxury: The biggest changeover you'll see going from a TX350 Premium to Luxury model is leather. It replaces the faux stuff in the lesser 350s. Additional standard content includes "thematic ambient lighting," 10-way power adjustment for the front seats, heated outboard rear seats, a heated steering wheel, and headlight washers. If you want six-passenger seating with second-row captain's chairs, you'll need to upgrade to this Luxury spec, by the way. The price is $60,950, plus $1,600 for AWD.

TX500h F Sport Performance Pricing and Features

  • TX500h Premium: The TX500h comes standard with all-wheel drive and six-passenger seating, presumably in keeping with its zestier vibe relative to the other TXs, albeit slathered in NuLuxe faux leather. (You can't get a 500h with seven-passenger seating, in fact.) Lexus sprinkles a bunch of F Sport bits, from sport pedals to the steering wheel to the dark gray 22-inch wheels. It is otherwise equipped like the TX350 Luxury, plus heated and ventilated second-row seats, and starts at $69,350.
  • TX500h Luxury: For actual leather, you'll want the $72,650 TX500h Luxury, which replaces NuLuxe with real stuff. It also includes a heated steering wheel, panoramic sunroof, 21-speaker Mark Levinson audio system, and unique 22-inch wheels.

Option packages include the Technology package (12.3-inch gauge cluster screen, 360-degree camera, self-parking, digital rearview mirror, and head-up display, depending on the trim it's added to) for between $1,050 and $2,380; the $895 Convenience package (digital key, front cross-traffic alert, traffic jam assist); and $250 Cold Area package (windshield wiper de-icer, heated wheel on models not equipped).

Compared to the Acura MDX, Audi Q7, and Infiniti QX60, the TX is priced in the thick of things. The Acura starts at $51,045, the Infiniti at $50,845, and the Audi is way out there at $60,695. That puts the TX right in the mix as a compelling option for shoppers.

`; export const simpleHtml = `

Space

`; +export const htmlWithImage = `

2024 Lexus TX 13
SEE ALL 27 PHOTOS

`; + export const volvoArticle = `

Volvo’s Cheapest, Quickest New Vehicle Also Is All-Electric

The entry-level 2025 Volvo EX30 is genuinely affordable—EV or not.

diff --git a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts index e52c6dc9f6f..f14a1a9804f 100644 --- a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts +++ b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts @@ -1,12 +1,12 @@ /** * @jest-environment jsdom */ -import { parseToLexicalObject } from "~/index"; +import { parseHtmlToLexical } from "~/index"; import { simpleHtml } from "./html-articles"; describe("Test html-to-lexical parser", () => { it("should parse html string to lexical object", async () => { - parseToLexicalObject(simpleHtml, data => { + parseHtmlToLexical(simpleHtml, data => { expect(data).toMatchObject({ root: { children: [ diff --git a/packages/html-to-lexical-parser/__tests__/volvo-article.html b/packages/html-to-lexical-parser/__tests__/volvo-article.html deleted file mode 100644 index 3b1745ebcb9..00000000000 --- a/packages/html-to-lexical-parser/__tests__/volvo-article.html +++ /dev/null @@ -1,296 +0,0 @@ -
-

Volvo’s Cheapest, Quickest New Vehicle Also Is - All-Electric

The entry-level 2025 Volvo EX30 is genuinely affordable—EV or not.

-
-
-
-
-
- -
-
-
- - -
- -
-

Of all the cool things we - learned about the 2025 Volvo EX30 when we got our first look in June, its - totally reasonable $36,000 entry point was the headliner. Full pricing, now revealed, - confirms that the upcoming EX30 stands as Volvo's cheapest and fastest-accelerating - vehicle—albeit its smallest—and it's electric.

-
-
- -
-
-

What Is the Volvo EX30?

-

The brand-new 2025 Volvo - EX30 is an all-electric sub-compact SUV. It's Volvo's smallest-ever SUV (it's the length - of a Jeep Renegade!) and Volvo's least expensive electric SUV (and least-expensive - vehicle, period) in the U.S. In terms of looks, it kind of resembles a baby version of - the EX90 electric three-row SUV, although it's built on a new platform called SEA - (Scalable Electric Architecture or Sustainable Experience Architecture) developed with - Chinese parent company Geely rather than the Volvo-designed SPA2 platform of the EX90. - Just for funsies, it'll be offered in Moss Yellow, Cloud Blue, Vapour Grey, Crystal - White, or Onyx Black exterior colors, with Mist, Pine, Indigo, and Breeze interiors.

-
-

Why Is the EX30 So Cheap?

-
-

Considering Volvo's EX30 - gets hit with the 25 percent tariff (and probably won't qualify for U.S. federal tax - credits) because it's built in China, it's completely necessary that this luxury-branded - electric SUV starts in the mid-$30,000s to stay competitive. It's built in China on a Geely-based - platform, which means cheaper labor and a shared platform, contributing to a - cheaper price tag. Furthermore, a rather symmetrical interior (for easier crossover - between right- and left-hand drive markets) and a concerted effort to keep parts at a - minimum (think sparsity of interior buttons and switches) also keep costs down while - shrinking its environmental footprint. These are the kinds of techniques employed by, - you guessed it, Tesla, on that automaker's less expensive Model 3 and Model Y. And most - obvious of all, the EX30 is physically exceedingly small, essentially a subcompact SUV, - a generally advantageous trait when it comes to price.

-

Single Motor Extended Range EX30: - $36,245

-

The first EX30 powertrain, - the Single Motor Extended Range, consists of one motor delivering 268 hp and 253 lb-ft - of torque to the rear wheels. It should accelerate to 60 mph in 5.1 seconds, although we - haven't tested one yet to confirm. Range comes in at 275 miles.

-

The Single Motor Extended - Range comes in Core, Plus, and Ultra equipment levels (trims). The base Core ($36,245) - comes with a large 12.3-inch tablet-style central display screen with Google built-in - (Google Maps and Google Assistant), wireless Apple CarPlay, and a bevy of advanced - safety features (including a driver alert system, blind spot information system, and - front and rear collision mitigation).

-

Upgrading to Plus ($40,195) - affords a premium Harman Kardon sound system, a dual top panoramic roof in Onyx Black, - and 19-inch wheels.

-

The Ultra ($41,895) tacks on - the next generation of Pilot Assist with Lane Change Assistance, a 360-degree camera - with a 3D view, and Park Pilot Assist.

-
-
-
-
-
-
-
-
-
-

Twin Motor Performance EX30: - $46,195

-

The second EX30 powertrain, - the Twin Motor Performance, is a 422-hp (154-hp front, 268 hp rear), 400 lb-ft two-motor - all-wheel-drive performance iteration of the tiny-but-mighty SUV. With a 0-60 time of - 3.4 seconds, it's hailed as Volvo's quickest car ever and should rival the larger, - 576-hp Kia EV6 GT. Range is expected to be 260 miles. Both EX30 powertrains are fed by a - 69-kWh Li-NMC battery that can charge from 10 to 80 percent in about 27 minutes with a - peak charging rate of 153 kW on the Twin Motor Performance.

-

The Twin Motor Performance - comes in Plus ($46,195) and Ultra ($47,895). Simple math tells us that extra motor comes - at a $6,000 premium.

-

Expect deliveries of the - Volvo EX30 to begin in the summer of 2024.

-

How Much Is the 2025 Volvo EX30? -

-

Single Motor - Extended Range

-
-
-
    -
  • Core - $36,245
  • -
  • Plus - $40,195
  • -
  • Ultra - $41,895
  • -
-
-
-

Twin Motor - Performance

-
-
-
    -
  • Plus - $46,195
  • -
  • Ultra - $47,895
  • -
-
-
-

-

How Much Is the Volvo EX30 Cross - Country?

-

Pricing is still forthcoming - for - the 2025 Volvo EX30 Cross Country, - which adds ground clearance, skidplates, 19-inch wheels (18s optional), and aggressive - black plastic cladding. The Cross Country should be available for order next year.

-
-
-
- - -
- -
- - -
-
diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts index b174dc753e1..b2d20f720a1 100644 --- a/packages/html-to-lexical-parser/src/index.ts +++ b/packages/html-to-lexical-parser/src/index.ts @@ -3,15 +3,22 @@ import { $generateNodesFromDOM } from "@lexical/html"; import { $getRoot, $getSelection } from "lexical"; import { allNodes } from "@webiny/lexical-editor/nodes/allNodes"; import { $createParagraphNode } from "@webiny/lexical-editor/nodes/ParagraphNode"; +import { Configuration } from "~/types"; const jsdom = require("jsdom"); const { JSDOM } = jsdom; +let configuration: Configuration = { nodes: [] }; + +export const configureParser = (config: Configuration) => { + configuration = config; +}; + /** * Parse html string to lexical object. - * This parser by default uses the Webiny lexical nodes. + * Note: This parser by default uses the Webiny lexical nodes. */ -export const parseToLexicalObject = ( +export const parseHtmlToLexical = ( htmlString: string, onSuccess: (data: Record) => void, onError?: (onError: Error) => void @@ -21,7 +28,7 @@ export const parseToLexicalObject = ( } const editor = createHeadlessEditor({ - nodes: allNodes, + nodes: [...allNodes, ...(configuration?.nodes || [])], onError: onError }); @@ -29,17 +36,17 @@ export const parseToLexicalObject = ( async () => { // Generate dom tree const dom = new JSDOM(htmlString); - // Convert to lexical node objects format that can be stored in db. + // Convert to lexical node objects that can be stored in db. const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document).map(node => { /** * Text node alone without parent element node(like paragraph) is not valid node. In that case * lexical will throw an error. - * In the code below, to fix this issue, we append the text node inside the paragraph node(parent element). + * To fix this issue, we append the text node inside the paragraph node(parent element). * - * Case when text node don't have parent element: + * In case when text node doesn't have parent element: * - * When we parse the DOM, sometimes, 'span' html tags don't have parent elements that match the - * lexical node elements, like paragraph or headings. In that case lexical will parse the span + * When we parse the DOM, sometimes, 'span' html tag doesn't have parent elements that match the + * lexical node elements, like paragraph or headings. In that case lexical will parse the 'span' tag * as text node, but without parent element. */ if (node.getType() === "text" && node.getParent() === null) { @@ -53,7 +60,7 @@ export const parseToLexicalObject = ( // Select the root $getRoot().select(); - // Insert them at a selection. + // Insert the nodes at a selection. const selection = $getSelection(); if (selection) { selection.insertNodes(lexicalNodes); diff --git a/packages/html-to-lexical-parser/src/types.ts b/packages/html-to-lexical-parser/src/types.ts new file mode 100644 index 00000000000..d66ad695cf6 --- /dev/null +++ b/packages/html-to-lexical-parser/src/types.ts @@ -0,0 +1,11 @@ +import { Klass, LexicalNode } from "lexical"; + +export type Configuration = { + nodes?: ReadonlyArray< + | Klass + | { + replace: Klass; + with: (node: InstanceType) => LexicalNode; + } + >; +}; diff --git a/packages/lexical-editor/src/nodes/ImageNode.tsx b/packages/lexical-editor/src/nodes/ImageNode.tsx index 60a19b3e54e..545edaf2d23 100644 --- a/packages/lexical-editor/src/nodes/ImageNode.tsx +++ b/packages/lexical-editor/src/nodes/ImageNode.tsx @@ -17,7 +17,6 @@ import type { SerializedLexicalNode, Spread } from "lexical"; - import { $applyNodeReplacement, createEditor, DecoratorNode } from "lexical"; import * as React from "react"; import { Suspense } from "react"; @@ -102,8 +101,17 @@ export class ImageNode extends DecoratorNode { return { element }; } + /** + * Control how an HTMLElement is represented in Lexical. + * DOM data comes from clipboard or parsing HTML to nodes with the available lexical functions. + * (@see @lexical/html package: https://github.com/facebook/lexical/blob/main/packages/lexical-html/README.md). + */ static importDOM(): DOMConversionMap | null { - // prevent paste from clipboard + /** + * By returning 'null' value, we are preventing image node to be created. + * Example of how to implement and create the node: + * https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/ImageNode.tsx#L94 + */ return null; } @@ -158,7 +166,6 @@ export class ImageNode extends DecoratorNode { } // View - override createDOM(config: EditorConfig): HTMLElement { const span = document.createElement("span"); const theme = config.theme; From 93b59c8a195d590c19eba55e911bfc32d2389bc1 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Fri, 6 Oct 2023 14:35:38 +0200 Subject: [PATCH 14/31] wip: readme, configuration refactor --- packages/html-to-lexical-parser/README.md | 93 +++++++++++++++++-- .../__tests__/html-to-lexical.test.ts | 62 ++++++------- packages/html-to-lexical-parser/src/index.ts | 32 +++---- packages/html-to-lexical-parser/src/types.ts | 14 +-- 4 files changed, 131 insertions(+), 70 deletions(-) diff --git a/packages/html-to-lexical-parser/README.md b/packages/html-to-lexical-parser/README.md index 89c5874e20d..3ad58fba54e 100644 --- a/packages/html-to-lexical-parser/README.md +++ b/packages/html-to-lexical-parser/README.md @@ -7,7 +7,7 @@ ## About -This package provides method for parsing html to lexical. +This package provides method for parsing html to lexical object. ## Usage @@ -17,18 +17,91 @@ the html string. ```tsx import {parseHtmlToLexical} from "@webiny/lexical-html-to-lexical-parser"; -const html = "

My paragraph

"; +const htmlString = "

My paragraph

"; +const lexicalObject = parseHtmlToLexical(htmlString); +``` + +Here is the result in JSON format. This object structure is a valid Lexical editor state. -parseHtmlToLexical(html, data => { - // success - console.log("data is parsed") - }, - error => { - // error - console.log("error", error.message); - }); +```json +{ + "root": { + "children": [ + { + "children": [ + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "Space", + "type": "text", + "version": 1 + } + ], + "direction": null, + "format": "", + "indent": 0, + "styles": [], + "type": "paragraph-element", + "version": 1 + } + ], + "direction": null, + "format": "", + "indent": 0, + "type": "root", + "version": 1 + } +} ``` +Next, you can import the parsed Lexical JSON in the Headless CMS app. +To find more about how to use our GraphQl API please check +our [GraphQL API Overview](https://www.webiny.com/docs/headless-cms/basics/graphql-api) article. + +## Configuration + +To configure the parser import `configureParser` method and provide the app level configuration options. + +```ts +import {configureParser} from "@webiny/lexical-html-to-lexical-parser"; +import {myCustomTheme} from "./theme/myCustomTheme"; +import {MyCustomLexicalNode} from './lexical/nodes/MyCustomLexicalNode' + +// App level configuration. +configureParser({ + // Lexical editor configuration + editorConfig: { + // Add custom nodes for parsing + nodes: [MyCustomLexicalNode], + // Add you custom theme + theme: myCustomTheme + } +}) +``` + +To learn more about how to create custom Lexical nodes or theme, please +visit [Lexical's documentation web page](https://lexical.dev/docs/intro). + +### Configuration options + +Configuration uses the `ParserConfigurationOptions` interface to define the configuration options for the parser. + +| Property | Type | Default value | Description | +|--------------|------------------|---------------|------------------------------------------------------------------| +| editorConfig | CreateEditorArgs | { nodes: [] } | Provide the original lexical interface for editor configuration. | + +By providing the `editorConfig` configuration, we can add custom Lexical nodes, custom theme and other editor related +options. + +> By default, this parser configuration includes all lexical nodes from the @webiny/lexical-editor package. + +Please check the full type definition, of the Lexical `CreateEditorArgs`, on the +following [link](https://lexical.dev/docs/api/modules/lexical#createeditorargs). + + + diff --git a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts index f14a1a9804f..23258c68e27 100644 --- a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts +++ b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts @@ -5,38 +5,36 @@ import { parseHtmlToLexical } from "~/index"; import { simpleHtml } from "./html-articles"; describe("Test html-to-lexical parser", () => { - it("should parse html string to lexical object", async () => { - parseHtmlToLexical(simpleHtml, data => { - expect(data).toMatchObject({ - root: { - children: [ - { - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "Space", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - styles: [], - type: "paragraph-element", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); + it("should parse html paragraph string to lexical object", async () => { + expect(parseHtmlToLexical(simpleHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "Space", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + styles: [], + type: "paragraph-element", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } }); }); }); diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts index b2d20f720a1..67bd16e4262 100644 --- a/packages/html-to-lexical-parser/src/index.ts +++ b/packages/html-to-lexical-parser/src/index.ts @@ -3,33 +3,32 @@ import { $generateNodesFromDOM } from "@lexical/html"; import { $getRoot, $getSelection } from "lexical"; import { allNodes } from "@webiny/lexical-editor/nodes/allNodes"; import { $createParagraphNode } from "@webiny/lexical-editor/nodes/ParagraphNode"; -import { Configuration } from "~/types"; +import { ParserConfigurationOptions } from "~/types"; const jsdom = require("jsdom"); const { JSDOM } = jsdom; -let configuration: Configuration = { nodes: [] }; +let configuration: ParserConfigurationOptions = { editorConfig: { nodes: [] } }; -export const configureParser = (config: Configuration) => { +/** + * Lexical editor configuration on application level. + * The default configuration includes all lexical nodes used in @webiny/lexical-editor package. + */ +export const configureParser = (config: ParserConfigurationOptions) => { configuration = config; }; /** - * Parse html string to lexical object. - * Note: This parser by default uses the Webiny lexical nodes. + * Parse html string to lexical JSON object. */ -export const parseHtmlToLexical = ( - htmlString: string, - onSuccess: (data: Record) => void, - onError?: (onError: Error) => void -): void => { +export const parseHtmlToLexical = (htmlString: string): Record | null => { if (!htmlString?.length) { - return; + return null; } const editor = createHeadlessEditor({ - nodes: [...allNodes, ...(configuration?.nodes || [])], - onError: onError + ...configuration.editorConfig, + nodes: [...allNodes, ...(configuration?.editorConfig?.nodes || [])] }); editor.update( @@ -43,8 +42,7 @@ export const parseHtmlToLexical = ( * lexical will throw an error. * To fix this issue, we append the text node inside the paragraph node(parent element). * - * In case when text node doesn't have parent element: - * + * The case when text node doesn't have parent element: * When we parse the DOM, sometimes, 'span' html tag doesn't have parent elements that match the * lexical node elements, like paragraph or headings. In that case lexical will parse the 'span' tag * as text node, but without parent element. @@ -72,7 +70,5 @@ export const parseHtmlToLexical = ( { discrete: true } ); - editor.getEditorState().read(() => { - onSuccess(editor.getEditorState().toJSON()); - }); + return editor.getEditorState().toJSON(); }; diff --git a/packages/html-to-lexical-parser/src/types.ts b/packages/html-to-lexical-parser/src/types.ts index d66ad695cf6..e064e0cdb84 100644 --- a/packages/html-to-lexical-parser/src/types.ts +++ b/packages/html-to-lexical-parser/src/types.ts @@ -1,11 +1,5 @@ -import { Klass, LexicalNode } from "lexical"; +import { CreateEditorArgs } from "lexical"; -export type Configuration = { - nodes?: ReadonlyArray< - | Klass - | { - replace: Klass; - with: (node: InstanceType) => LexicalNode; - } - >; -}; +export interface ParserConfigurationOptions { + editorConfig?: CreateEditorArgs; +} From b1bc889a15fc99ed4302e4cf0e45d3ec5ac0a389 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Fri, 6 Oct 2023 16:24:12 +0200 Subject: [PATCH 15/31] wip: type mapper --- packages/html-to-lexical-parser/README.md | 4 +- .../__tests__/html-to-lexical.test.ts | 5 +- packages/html-to-lexical-parser/src/index.ts | 107 +++++++++--------- packages/html-to-lexical-parser/src/types.ts | 6 +- 4 files changed, 61 insertions(+), 61 deletions(-) diff --git a/packages/html-to-lexical-parser/README.md b/packages/html-to-lexical-parser/README.md index 3ad58fba54e..f570f481520 100644 --- a/packages/html-to-lexical-parser/README.md +++ b/packages/html-to-lexical-parser/README.md @@ -88,6 +88,8 @@ visit [Lexical's documentation web page](https://lexical.dev/docs/intro). Configuration uses the `ParserConfigurationOptions` interface to define the configuration options for the parser. +By default, this parser configuration includes all lexical nodes from the @webiny/lexical-editor package. + | Property | Type | Default value | Description | |--------------|------------------|---------------|------------------------------------------------------------------| | editorConfig | CreateEditorArgs | { nodes: [] } | Provide the original lexical interface for editor configuration. | @@ -95,8 +97,6 @@ Configuration uses the `ParserConfigurationOptions` interface to define the conf By providing the `editorConfig` configuration, we can add custom Lexical nodes, custom theme and other editor related options. -> By default, this parser configuration includes all lexical nodes from the @webiny/lexical-editor package. - Please check the full type definition, of the Lexical `CreateEditorArgs`, on the following [link](https://lexical.dev/docs/api/modules/lexical#createeditorargs). diff --git a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts index 23258c68e27..e92ab84f798 100644 --- a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts +++ b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts @@ -1,12 +1,13 @@ /** * @jest-environment jsdom */ -import { parseHtmlToLexical } from "~/index"; +import { createHtmlToLexicalParser } from "~/index"; import { simpleHtml } from "./html-articles"; describe("Test html-to-lexical parser", () => { it("should parse html paragraph string to lexical object", async () => { - expect(parseHtmlToLexical(simpleHtml)).toMatchObject({ + const parser = createHtmlToLexicalParser(); + expect(parser(simpleHtml)).toMatchObject({ root: { children: [ { diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts index 67bd16e4262..a1b01ed045b 100644 --- a/packages/html-to-lexical-parser/src/index.ts +++ b/packages/html-to-lexical-parser/src/index.ts @@ -8,67 +8,62 @@ import { ParserConfigurationOptions } from "~/types"; const jsdom = require("jsdom"); const { JSDOM } = jsdom; -let configuration: ParserConfigurationOptions = { editorConfig: { nodes: [] } }; - -/** - * Lexical editor configuration on application level. - * The default configuration includes all lexical nodes used in @webiny/lexical-editor package. - */ -export const configureParser = (config: ParserConfigurationOptions) => { - configuration = config; -}; - /** * Parse html string to lexical JSON object. */ -export const parseHtmlToLexical = (htmlString: string): Record | null => { - if (!htmlString?.length) { - return null; - } +// // convert in factory - createHtmlToLexicalParser(config) +export const createHtmlToLexicalParser = + (config?: ParserConfigurationOptions) => + (htmlString: string): Record | null => { + if (!htmlString?.length) { + return null; + } - const editor = createHeadlessEditor({ - ...configuration.editorConfig, - nodes: [...allNodes, ...(configuration?.editorConfig?.nodes || [])] - }); + const editor = createHeadlessEditor({ + ...config?.editorConfig, + nodes: [...allNodes, ...(config?.editorConfig?.nodes || [])] + }); - editor.update( - async () => { - // Generate dom tree - const dom = new JSDOM(htmlString); - // Convert to lexical node objects that can be stored in db. - const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document).map(node => { - /** - * Text node alone without parent element node(like paragraph) is not valid node. In that case - * lexical will throw an error. - * To fix this issue, we append the text node inside the paragraph node(parent element). - * - * The case when text node doesn't have parent element: - * When we parse the DOM, sometimes, 'span' html tag doesn't have parent elements that match the - * lexical node elements, like paragraph or headings. In that case lexical will parse the 'span' tag - * as text node, but without parent element. - */ - if (node.getType() === "text" && node.getParent() === null) { - const paragraphNode = $createParagraphNode(); - paragraphNode.append(node); - return paragraphNode; - } - return node; - }); + editor.update( + () => { + // Generate dom tree + const dom = new JSDOM(htmlString); + // Convert to lexical node objects that can be stored in db. + const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document).map( + node => { + /** + * Text node alone without parent element node(like paragraph) is not valid node. In that case + * lexical will throw an error. + * To fix this issue, we append the text node inside the paragraph node(parent element). + * + * The case when text node doesn't have parent element: + * When we parse the DOM, sometimes, 'span' html tag doesn't have parent elements that match the + * lexical node elements, like paragraph or headings. In that case lexical will parse the 'span' tag + * as text node, but without parent element. + */ + if (node.getType() === "text" && node.getParent() === null) { + const paragraphNode = $createParagraphNode(); + paragraphNode.append(node); + return paragraphNode; + } + return node; + } + ); - // Select the root - $getRoot().select(); + // Select the root + $getRoot().select(); - // Insert the nodes at a selection. - const selection = $getSelection(); - if (selection) { - selection.insertNodes(lexicalNodes); - } - }, - /** - * discrete – If true, prevents this update from being batched, forcing it to run synchronously. - */ - { discrete: true } - ); + // Insert the nodes at a selection. + const selection = $getSelection(); + if (selection) { + selection.insertNodes(lexicalNodes); + } + }, + /** + * Prevents this update from being batched, forcing it to run synchronously. + */ + { discrete: true } + ); - return editor.getEditorState().toJSON(); -}; + return editor.getEditorState().toJSON(); + }; diff --git a/packages/html-to-lexical-parser/src/types.ts b/packages/html-to-lexical-parser/src/types.ts index e064e0cdb84..cf219efdff0 100644 --- a/packages/html-to-lexical-parser/src/types.ts +++ b/packages/html-to-lexical-parser/src/types.ts @@ -1,5 +1,9 @@ -import { CreateEditorArgs } from "lexical"; +import { CreateEditorArgs, LexicalNode } from "lexical"; +import { $generateNodesFromDOM } from "@lexical/html"; + +type NodeMapper = ReturnType; export interface ParserConfigurationOptions { editorConfig?: CreateEditorArgs; + nodeMapper: (node: LexicalNode) => NodeMapper; } From bb4929a3bc427dc729c3d55e5d36c99956eb6581 Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Fri, 6 Oct 2023 16:59:49 +0200 Subject: [PATCH 16/31] wip: make parser configurable --- packages/html-to-lexical-parser/src/index.ts | 67 +++++++++++--------- packages/html-to-lexical-parser/src/types.ts | 6 +- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts index a1b01ed045b..aa7fca217b4 100644 --- a/packages/html-to-lexical-parser/src/index.ts +++ b/packages/html-to-lexical-parser/src/index.ts @@ -1,54 +1,60 @@ +// @ts-ignore "@types/jsdom" is disabled in the root package.json resolutions. +import jsdom from "jsdom"; import { createHeadlessEditor } from "@lexical/headless"; import { $generateNodesFromDOM } from "@lexical/html"; import { $getRoot, $getSelection } from "lexical"; import { allNodes } from "@webiny/lexical-editor/nodes/allNodes"; import { $createParagraphNode } from "@webiny/lexical-editor/nodes/ParagraphNode"; -import { ParserConfigurationOptions } from "~/types"; +import { NodeMapper, ParserConfigurationOptions } from "~/types"; -const jsdom = require("jsdom"); -const { JSDOM } = jsdom; +/** + * Text node alone, without parent element node (like "paragraph"), is not a valid node, and Lexical will throw an error. + * To fix this issue, we wrap the text node with a paragraph node (create a parent element for it). + * + * EXAMPLE: + * When we parse the DOM, sometimes, 'span' html tag doesn't have parent elements that match the + * lexical node elements (there's no Node class that can handle that HTML element), like paragraph or headings. + * In this case, Lexical will parse the 'span' tag as a text node, but without a parent element. + */ +const textNodeParentNormalizer: NodeMapper = node => { + if (node.getType() === "text" && node.getParent() === null) { + return $createParagraphNode().append(node); + } + return node; +}; + +const passthroughMapper: NodeMapper = node => node; /** * Parse html string to lexical JSON object. */ -// // convert in factory - createHtmlToLexicalParser(config) -export const createHtmlToLexicalParser = - (config?: ParserConfigurationOptions) => - (htmlString: string): Record | null => { +export const createHtmlToLexicalParser = (config: ParserConfigurationOptions = {}) => { + return (htmlString: string): Record | null => { if (!htmlString?.length) { return null; } + const normalizeTextNodes = config.normalizeTextNodes ?? true; + const textNodeNormalizer = normalizeTextNodes + ? textNodeParentNormalizer + : passthroughMapper; + + const customNodeMapper: NodeMapper = config.nodeMapper ?? passthroughMapper; + const editor = createHeadlessEditor({ - ...config?.editorConfig, - nodes: [...allNodes, ...(config?.editorConfig?.nodes || [])] + ...config.editorConfig, + nodes: [...allNodes, ...(config.editorConfig?.nodes || [])] }); editor.update( () => { // Generate dom tree - const dom = new JSDOM(htmlString); + const dom = new jsdom.JSDOM(htmlString); + // Convert to lexical node objects that can be stored in db. - const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document).map( - node => { - /** - * Text node alone without parent element node(like paragraph) is not valid node. In that case - * lexical will throw an error. - * To fix this issue, we append the text node inside the paragraph node(parent element). - * - * The case when text node doesn't have parent element: - * When we parse the DOM, sometimes, 'span' html tag doesn't have parent elements that match the - * lexical node elements, like paragraph or headings. In that case lexical will parse the 'span' tag - * as text node, but without parent element. - */ - if (node.getType() === "text" && node.getParent() === null) { - const paragraphNode = $createParagraphNode(); - paragraphNode.append(node); - return paragraphNode; - } - return node; - } - ); + const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document) + .map(textNodeNormalizer) + .map(customNodeMapper); // Select the root $getRoot().select(); @@ -67,3 +73,4 @@ export const createHtmlToLexicalParser = return editor.getEditorState().toJSON(); }; +}; diff --git a/packages/html-to-lexical-parser/src/types.ts b/packages/html-to-lexical-parser/src/types.ts index cf219efdff0..9d7bcebcd05 100644 --- a/packages/html-to-lexical-parser/src/types.ts +++ b/packages/html-to-lexical-parser/src/types.ts @@ -1,9 +1,9 @@ import { CreateEditorArgs, LexicalNode } from "lexical"; -import { $generateNodesFromDOM } from "@lexical/html"; -type NodeMapper = ReturnType; +export type NodeMapper = (node: LexicalNode) => LexicalNode; export interface ParserConfigurationOptions { editorConfig?: CreateEditorArgs; - nodeMapper: (node: LexicalNode) => NodeMapper; + nodeMapper?: NodeMapper; + normalizeTextNodes?: boolean; } From 2815238a3a4129cb0aec63083afd82d8d685d412 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Mon, 9 Oct 2023 12:31:47 +0200 Subject: [PATCH 17/31] wip: node unit tests part 1 --- .../__tests__/configuration.tets.ts | 0 .../html-to-lexical-parser/__tests__/data.ts | 18 + .../__tests__/html-to-lexical.test.ts | 385 +++++++++++++++++- 3 files changed, 396 insertions(+), 7 deletions(-) create mode 100644 packages/html-to-lexical-parser/__tests__/configuration.tets.ts create mode 100644 packages/html-to-lexical-parser/__tests__/data.ts diff --git a/packages/html-to-lexical-parser/__tests__/configuration.tets.ts b/packages/html-to-lexical-parser/__tests__/configuration.tets.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/html-to-lexical-parser/__tests__/data.ts b/packages/html-to-lexical-parser/__tests__/data.ts new file mode 100644 index 00000000000..84bf7870653 --- /dev/null +++ b/packages/html-to-lexical-parser/__tests__/data.ts @@ -0,0 +1,18 @@ +export const paragraphHtmlTag = `

Testing paragraph element

`; +export const htmlWithNotSupportedHtmlTags = `
+
+ +
+
`; + +export const boldItalicUnderlineFormatHtml = `

formatted text

`; + +export const headingH1Html = `

Testing heading h1 element

`; +export const headingH4Html = `

Testing heading h4 element

`; + +export const bulletListHtml = `
  • list item 1
  • list item 2
`; +export const numberedListHtml = `
  1. list item 1
  2. list item 2
`; +export const linkHtml = `

My webiny link

`; +export const quoteHtml = `
My quote block
`; diff --git a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts index e92ab84f798..4d6744b1067 100644 --- a/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts +++ b/packages/html-to-lexical-parser/__tests__/html-to-lexical.test.ts @@ -1,13 +1,25 @@ /** * @jest-environment jsdom */ -import { createHtmlToLexicalParser } from "~/index"; -import { simpleHtml } from "./html-articles"; -describe("Test html-to-lexical parser", () => { - it("should parse html paragraph string to lexical object", async () => { - const parser = createHtmlToLexicalParser(); - expect(parser(simpleHtml)).toMatchObject({ +import { + boldItalicUnderlineFormatHtml, + bulletListHtml, + headingH1Html, + headingH4Html, + htmlWithNotSupportedHtmlTags, + linkHtml, + numberedListHtml, + paragraphHtmlTag, + quoteHtml +} from "./data"; +import { createHtmlToLexicalParser } from "../src/index"; + +const parser = createHtmlToLexicalParser(); + +describe("Test how parser convert the html tags into webiny's lexical node objects", () => { + it("should parse html to paragraph node", async () => { + expect(parser(paragraphHtmlTag)).toMatchObject({ root: { children: [ { @@ -17,7 +29,7 @@ describe("Test html-to-lexical parser", () => { format: 0, mode: "normal", style: "", - text: "Space", + text: "Testing paragraph element", type: "text", version: 1 } @@ -39,3 +51,362 @@ describe("Test html-to-lexical parser", () => { }); }); }); +it("should normalize the text node - wrap the node in paragraph element", () => { + /** + * By default, parser will normalize the nodes and convert DOM nodes to supported lexical nodes. + * It's expected all unsupported html tags like div, figure, button and other to be removed and not converted. + */ + expect(parser(htmlWithNotSupportedHtmlTags)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "See all 37 photos", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + styles: [], + type: "paragraph-element", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); +}); + +it("should generate paragraph element with text node with bold, italic and underline formats", () => { + expect(parser(boldItalicUnderlineFormatHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 11, // all formats + mode: "normal", + style: "", + text: "formatted text", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "paragraph-element", + version: 1, + styles: [] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); +}); + +it("should generate heading nodes", () => { + /** + * Test HTML h1 tag + */ + expect(parser(headingH1Html)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "Testing heading h1 element", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + tag: "h1", + type: "heading-element", + version: 1, + styles: [] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + + /** + * Test HTML h4 tag + */ + expect(parser(headingH4Html)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "Testing heading h4 element", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + tag: "h4", + type: "heading-element", + version: 1, + styles: [] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); +}); + +it("should generate bullet list, and nested list item nodes", () => { + expect(parser(bulletListHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + checked: undefined, + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "list item 1", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-listitem", + version: 1, + value: 1 + }, + { + checked: undefined, + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "list item 2", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-listitem", + version: 1, + value: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-list", + version: 1, + listType: "bullet", + start: 1, + tag: "ul", + themeStyleId: "" + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); +}); + +it("should generate numbered list, and nested list item nodes", () => { + expect(parser(numberedListHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + checked: undefined, + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "list item 1", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-listitem", + version: 1, + value: 1 + }, + { + checked: undefined, + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "list item 2", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-listitem", + version: 1, + value: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-list", + version: 1, + listType: "number", + start: 1, + tag: "ol", + themeStyleId: "" + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); +}); + +it("should not generate paragraph and link node", () => { + expect(parser(linkHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "My webiny link", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "link-node", + version: 1, + rel: "noopener noreferrer", + target: "_blank", + title: null, + url: "https://webiny.com" + } + ], + direction: null, + format: "", + indent: 0, + type: "paragraph-element", + version: 1, + styles: [] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); +}); + +it("should not generate image node", () => {}); + +it("should generate quote node", () => { + expect(parser(quoteHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "My quote block", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-quote", + version: 1, + styles: [], + styleId: undefined + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); +}); + +it("should generate code node", () => {}); From 42d5153e5cdb937c77ec285b7cf9d93303e29d9a Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Mon, 9 Oct 2023 14:54:07 +0200 Subject: [PATCH 18/31] wip: add configuration configuration --- .../__tests__/configuration.test.ts | 65 +++++++++++++++++++ .../__tests__/configuration.tets.ts | 0 .../__tests__/html-articles.ts | 5 +- ....test.ts => html-to-lexical-nodes.test.ts} | 50 +++++++++++++- .../__tests__/{data.ts => test-data.ts} | 3 + packages/html-to-lexical-parser/src/index.ts | 4 +- packages/html-to-lexical-parser/src/types.ts | 4 +- 7 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 packages/html-to-lexical-parser/__tests__/configuration.test.ts delete mode 100644 packages/html-to-lexical-parser/__tests__/configuration.tets.ts rename packages/html-to-lexical-parser/__tests__/{html-to-lexical.test.ts => html-to-lexical-nodes.test.ts} (90%) rename packages/html-to-lexical-parser/__tests__/{data.ts => test-data.ts} (85%) diff --git a/packages/html-to-lexical-parser/__tests__/configuration.test.ts b/packages/html-to-lexical-parser/__tests__/configuration.test.ts new file mode 100644 index 00000000000..be3ce6ae6c4 --- /dev/null +++ b/packages/html-to-lexical-parser/__tests__/configuration.test.ts @@ -0,0 +1,65 @@ +import { createHtmlToLexicalParser } from "~/index"; +import { headingH1Html, htmlWithNotSupportedHtmlTags } from "./test-data"; +import { LexicalNode } from "lexical"; +import { HeadingNode } from "@webiny/lexical-editor/nodes/HeadingNode"; + +describe("Test how parser configuration options", () => { + it("should be able to turn off default node text node normalizer", async () => { + const parser = createHtmlToLexicalParser({ normalizeTextNodes: false }); + + /** By removing the default text node normalizer, text nodes can't exist alone, so we expect error to be thrown. + * Error #56 on the lexical website: https://lexical.dev/docs/error?code=56 + */ + try { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const nodes = parser(htmlWithNotSupportedHtmlTags); + } catch (e) { + expect(e.message).toMatch(e.message.contains("Minified Lexical error #56;")); + } + }); + + it("should be able to add custom node mapper", async () => { + const addCustomThemeStyleToHeadings = (node: LexicalNode): LexicalNode => { + if (node.getType() === "heading-element") { + return (node as HeadingNode).setThemeStyles([ + { styleId: "my-default-id", type: "typography" } + ]); + } + return node; + }; + + const parser = createHtmlToLexicalParser({ nodeMapper: addCustomThemeStyleToHeadings }); + expect(parser(headingH1Html)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "Testing heading h1 element", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + tag: "h1", + type: "heading-element", + version: 1, + // modified by node mapper + styles: [{ styleId: "my-default-id", type: "typography" }] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); +}); diff --git a/packages/html-to-lexical-parser/__tests__/configuration.tets.ts b/packages/html-to-lexical-parser/__tests__/configuration.tets.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/html-to-lexical-parser/__tests__/html-articles.ts b/packages/html-to-lexical-parser/__tests__/html-articles.ts index 73a6449719f..385ca5a44d7 100644 --- a/packages/html-to-lexical-parser/__tests__/html-articles.ts +++ b/packages/html-to-lexical-parser/__tests__/html-articles.ts @@ -47,9 +47,8 @@ export const volvoArticle = `
{ }); }); -it("should not generate image node", () => {}); +it("should not generate image node", () => { + expect(parser(imageHtml)).toMatchObject({ + root: { + children: [], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); +}); it("should generate quote node", () => { expect(parser(quoteHtml)).toMatchObject({ @@ -409,4 +422,35 @@ it("should generate quote node", () => { }); }); -it("should generate code node", () => {}); +it("should generate code node", () => { + expect(parser(codeHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 16, // lexical number for code format + mode: "normal", + style: "", + text: "Text code formatting", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "paragraph-element", + version: 1, + styles: [] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); +}); diff --git a/packages/html-to-lexical-parser/__tests__/data.ts b/packages/html-to-lexical-parser/__tests__/test-data.ts similarity index 85% rename from packages/html-to-lexical-parser/__tests__/data.ts rename to packages/html-to-lexical-parser/__tests__/test-data.ts index 84bf7870653..4642c6cd6be 100644 --- a/packages/html-to-lexical-parser/__tests__/data.ts +++ b/packages/html-to-lexical-parser/__tests__/test-data.ts @@ -16,3 +16,6 @@ export const bulletListHtml = `
  • list item 1
  • list item 2
export const numberedListHtml = `
  1. list item 1
  2. list item 2
`; export const linkHtml = `

My webiny link

`; export const quoteHtml = `
My quote block
`; +export const codeHtml = `Text code formatting`; + +export const imageHtml = `webiny image`; diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts index aa7fca217b4..c8276406b17 100644 --- a/packages/html-to-lexical-parser/src/index.ts +++ b/packages/html-to-lexical-parser/src/index.ts @@ -53,8 +53,8 @@ export const createHtmlToLexicalParser = (config: ParserConfigurationOptions = { // Convert to lexical node objects that can be stored in db. const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document) - .map(textNodeNormalizer) - .map(customNodeMapper); + .map(node => textNodeNormalizer(node)) + .map(node => customNodeMapper(node, editor)); // Select the root $getRoot().select(); diff --git a/packages/html-to-lexical-parser/src/types.ts b/packages/html-to-lexical-parser/src/types.ts index 9d7bcebcd05..b561682e575 100644 --- a/packages/html-to-lexical-parser/src/types.ts +++ b/packages/html-to-lexical-parser/src/types.ts @@ -1,6 +1,6 @@ -import { CreateEditorArgs, LexicalNode } from "lexical"; +import { CreateEditorArgs, LexicalEditor, LexicalNode } from "lexical"; -export type NodeMapper = (node: LexicalNode) => LexicalNode; +export type NodeMapper = (node: LexicalNode, editor?: LexicalEditor) => LexicalNode; export interface ParserConfigurationOptions { editorConfig?: CreateEditorArgs; From 64d515eea6e6d366338ced653380dfb272555172 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Mon, 9 Oct 2023 15:09:31 +0200 Subject: [PATCH 19/31] wip: readme update --- packages/html-to-lexical-parser/README.md | 22 +- .../__tests__/html-articles.ts | 300 ------------------ 2 files changed, 18 insertions(+), 304 deletions(-) delete mode 100644 packages/html-to-lexical-parser/__tests__/html-articles.ts diff --git a/packages/html-to-lexical-parser/README.md b/packages/html-to-lexical-parser/README.md index f570f481520..817b0fbcf30 100644 --- a/packages/html-to-lexical-parser/README.md +++ b/packages/html-to-lexical-parser/README.md @@ -69,6 +69,15 @@ import {configureParser} from "@webiny/lexical-html-to-lexical-parser"; import {myCustomTheme} from "./theme/myCustomTheme"; import {MyCustomLexicalNode} from './lexical/nodes/MyCustomLexicalNode' +const addCustomThemeStyleToHeadings = (node: LexicalNode): LexicalNode => { + if (node.getType() === "heading-element") { + return (node as HeadingNode).setThemeStyles([ + {styleId: "my-default-id", type: "typography"} + ]); + } + return node; +}; + // App level configuration. configureParser({ // Lexical editor configuration @@ -77,7 +86,10 @@ configureParser({ nodes: [MyCustomLexicalNode], // Add you custom theme theme: myCustomTheme - } + }, + nodeMapper: addCustomThemeStyleToHeadings, + normalizeTextNodes: true // by default is 'true' + }) ``` @@ -90,9 +102,11 @@ Configuration uses the `ParserConfigurationOptions` interface to define the conf By default, this parser configuration includes all lexical nodes from the @webiny/lexical-editor package. -| Property | Type | Default value | Description | -|--------------|------------------|---------------|------------------------------------------------------------------| -| editorConfig | CreateEditorArgs | { nodes: [] } | Provide the original lexical interface for editor configuration. | +| Prop | Type | Default value | Description | +|--------------------|---------------------------------------------------------|---------------|----------------------------------------------------------------------| +| editorConfig | CreateEditorArgs | { nodes: [] } | Configure the Lexical editor by providing the editor initialization | +| nodeMapper | ({ node: LexicalNode, editor: LexicalEditor } => void; | | Define custom mapper function for mapping lexical nodes. | +| normalizeTextNodes | boolean | true | By default, parser will wrap single text nodes with paragraph nodes. | By providing the `editorConfig` configuration, we can add custom Lexical nodes, custom theme and other editor related options. diff --git a/packages/html-to-lexical-parser/__tests__/html-articles.ts b/packages/html-to-lexical-parser/__tests__/html-articles.ts deleted file mode 100644 index 385ca5a44d7..00000000000 --- a/packages/html-to-lexical-parser/__tests__/html-articles.ts +++ /dev/null @@ -1,300 +0,0 @@ -export const carArticle = `

2024 Lexus TX Enters 3-Row SUV Price Brawl With Competitive MSRP

The all-new Lexus luxury SUV starts at a reasonable price for the segment.

Related Video

Lexus's first true midsize three-row SUV, the all-new TX, is almost here. With a properly boxy body atop car-based underpinnings, it's a far roomier offering than the half-baked, extended version of the last-generation two-row RX Lexus sold previously. It's also far more family friendly than the brand's body-on-frame, more-off-road-oriented GX and LX SUVs. Those may have had three rows of seating, but were aimed at a different swath of buyers than those shopping for mall crawlers like the Acura MDX, Audi Q7, Infiniti QX60, Volvo XC90, and so on. With the TX set to launch soon, we finally know how much it'll cost.

2024 Lexus TX Price? It's Nice

There will be several versions of the TX available at launch, with a third option coming later. The entry-level TX350 comes powered by a 275-hp turbocharged 2.4-liter four-cylinder engine; the TX500h F Sport Performance model uses a hybridized version of that 2.4-liter engine and produces a saucier 366 total hp; and coming later there is a TX550h+ plug-in hybrid that combines several electric motors with a 3.5-liter V-6 and delivers 406 hp.

Each of these TX variants is subdivided into narrower trim levels, with the TX350 available in base, Premium, and Luxury specifications, with each offered with front- or all-wheel-drive for an extra $1,600; the TX500h is only available with all-wheel-drive and in Premium and Luxury trims. TX550h+ details are forthcoming.

Lexus TX350 Pricing and Features

  • TX350: The least-expensive TX350, with front-wheel-drive and no options, starts at $55,050 including Lexus's rich $1,350 destination charge. Seven-passenger seating is standard, with eight-way power adjustment and heating on the fronts, and the cabin is slathered in Lexus's NuLuxe faux-leather. Proximity key access is included, as is a power-opening tailgate with hands-free sensor, manual rear side window sunshades, a wireless phone charger, 14-inch touchscreen, wireless Apple CarPlay and Android Auto, rain-sensing wipers, and Lexus's Safety System+ 3.0 active safety gear (adaptive cruise control, automated emergency braking, lane-keep assist, blind-spot monitoring, and auto high beams).
  • TX350 Premium: Besides opening access to a number of higher-end option packages, the Premium trim adds as standard unique 20-inch wheels, a panoramic sunroof, power-folding and reclining third-row seats, and front-seat ventilation and memory function. The price rises to $58,450, with AWD again a $1,600 option.
  • TX350 Luxury: The biggest changeover you'll see going from a TX350 Premium to Luxury model is leather. It replaces the faux stuff in the lesser 350s. Additional standard content includes "thematic ambient lighting," 10-way power adjustment for the front seats, heated outboard rear seats, a heated steering wheel, and headlight washers. If you want six-passenger seating with second-row captain's chairs, you'll need to upgrade to this Luxury spec, by the way. The price is $60,950, plus $1,600 for AWD.

TX500h F Sport Performance Pricing and Features

  • TX500h Premium: The TX500h comes standard with all-wheel drive and six-passenger seating, presumably in keeping with its zestier vibe relative to the other TXs, albeit slathered in NuLuxe faux leather. (You can't get a 500h with seven-passenger seating, in fact.) Lexus sprinkles a bunch of F Sport bits, from sport pedals to the steering wheel to the dark gray 22-inch wheels. It is otherwise equipped like the TX350 Luxury, plus heated and ventilated second-row seats, and starts at $69,350.
  • TX500h Luxury: For actual leather, you'll want the $72,650 TX500h Luxury, which replaces NuLuxe with real stuff. It also includes a heated steering wheel, panoramic sunroof, 21-speaker Mark Levinson audio system, and unique 22-inch wheels.

Option packages include the Technology package (12.3-inch gauge cluster screen, 360-degree camera, self-parking, digital rearview mirror, and head-up display, depending on the trim it's added to) for between $1,050 and $2,380; the $895 Convenience package (digital key, front cross-traffic alert, traffic jam assist); and $250 Cold Area package (windshield wiper de-icer, heated wheel on models not equipped).

Compared to the Acura MDX, Audi Q7, and Infiniti QX60, the TX is priced in the thick of things. The Acura starts at $51,045, the Infiniti at $50,845, and the Audi is way out there at $60,695. That puts the TX right in the mix as a compelling option for shoppers.

`; -export const simpleHtml = `

Space

`; - -export const htmlWithImage = `

2024 Lexus TX 13
SEE ALL 27 PHOTOS

`; - -export const volvoArticle = `
-

Volvo’s Cheapest, Quickest New Vehicle Also Is - All-Electric

The entry-level 2025 Volvo EX30 is genuinely affordable—EV or not.

-
-
-
-
-
- -
-
-
- - -
- -
-

Of all the cool things we - learned about the 2025 Volvo EX30 when we got our first look in June, its - totally reasonable $36,000 entry point was the headliner. Full pricing, now revealed, - confirms that the upcoming EX30 stands as Volvo's cheapest and fastest-accelerating - vehicle—albeit its smallest—and it's electric.

-
-
- -
-
-

What Is the Volvo EX30?

-

The brand-new 2025 Volvo - EX30 is an all-electric sub-compact SUV. It's Volvo's smallest-ever SUV (it's the length - of a Jeep Renegade!) and Volvo's least expensive electric SUV (and least-expensive - vehicle, period) in the U.S. In terms of looks, it kind of resembles a baby version of - the EX90 electric three-row SUV, although it's built on a new platform called SEA - (Scalable Electric Architecture or Sustainable Experience Architecture) developed with - Chinese parent company Geely rather than the Volvo-designed SPA2 platform of the EX90. - Just for funsies, it'll be offered in Moss Yellow, Cloud Blue, Vapour Grey, Crystal - White, or Onyx Black exterior colors, with Mist, Pine, Indigo, and Breeze interiors.

-
-

Why Is the EX30 So Cheap?

-
-

Considering Volvo's EX30 - gets hit with the 25 percent tariff (and probably won't qualify for U.S. federal tax - credits) because it's built in China, it's completely necessary that this luxury-branded - electric SUV starts in the mid-$30,000s to stay competitive. It's built in China on a Geely-based - platform, which means cheaper labor and a shared platform, contributing to a - cheaper price tag. Furthermore, a rather symmetrical interior (for easier crossover - between right- and left-hand drive markets) and a concerted effort to keep parts at a - minimum (think sparsity of interior buttons and switches) also keep costs down while - shrinking its environmental footprint. These are the kinds of techniques employed by, - you guessed it, Tesla, on that automaker's less expensive Model 3 and Model Y. And most - obvious of all, the EX30 is physically exceedingly small, essentially a subcompact SUV, - a generally advantageous trait when it comes to price.

-

Single Motor Extended Range EX30: - $36,245

-

The first EX30 powertrain, - the Single Motor Extended Range, consists of one motor delivering 268 hp and 253 lb-ft - of torque to the rear wheels. It should accelerate to 60 mph in 5.1 seconds, although we - haven't tested one yet to confirm. Range comes in at 275 miles.

-

The Single Motor Extended - Range comes in Core, Plus, and Ultra equipment levels (trims). The base Core ($36,245) - comes with a large 12.3-inch tablet-style central display screen with Google built-in - (Google Maps and Google Assistant), wireless Apple CarPlay, and a bevy of advanced - safety features (including a driver alert system, blind spot information system, and - front and rear collision mitigation).

-

Upgrading to Plus ($40,195) - affords a premium Harman Kardon sound system, a dual top panoramic roof in Onyx Black, - and 19-inch wheels.

-

The Ultra ($41,895) tacks on - the next generation of Pilot Assist with Lane Change Assistance, a 360-degree camera - with a 3D view, and Park Pilot Assist.

-
-
-
-
-
-
-
-
-
-

Twin Motor Performance EX30: - $46,195

-

The second EX30 powertrain, - the Twin Motor Performance, is a 422-hp (154-hp front, 268 hp rear), 400 lb-ft two-motor - all-wheel-drive performance iteration of the tiny-but-mighty SUV. With a 0-60 time of - 3.4 seconds, it's hailed as Volvo's quickest car ever and should rival the larger, - 576-hp Kia EV6 GT. Range is expected to be 260 miles. Both EX30 powertrains are fed by a - 69-kWh Li-NMC battery that can charge from 10 to 80 percent in about 27 minutes with a - peak charging rate of 153 kW on the Twin Motor Performance.

-

The Twin Motor Performance - comes in Plus ($46,195) and Ultra ($47,895). Simple math tells us that extra motor comes - at a $6,000 premium.

-

Expect deliveries of the - Volvo EX30 to begin in the summer of 2024.

-

How Much Is the 2025 Volvo EX30? -

-

Single Motor - Extended Range

-
-
-
    -
  • Core - $36,245
  • -
  • Plus - $40,195
  • -
  • Ultra - $41,895
  • -
-
-
-

Twin Motor - Performance

-
-
-
    -
  • Plus - $46,195
  • -
  • Ultra - $47,895
  • -
-
-
-

-

How Much Is the Volvo EX30 Cross - Country?

-

Pricing is still forthcoming - for - the 2025 Volvo EX30 Cross Country, - which adds ground clearance, skidplates, 19-inch wheels (18s optional), and aggressive - black plastic cladding. The Cross Country should be available for order next year.

-
-
-
- - -
- -
- - -
-
`; From df148525d3c1e9f1fc22b0efdfc319c2cb7844ca Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Tue, 10 Oct 2023 12:41:30 +0200 Subject: [PATCH 20/31] wip: updated readme and unit test - turn off the lexical default normalizer --- packages/html-to-lexical-parser/README.md | 56 +++++++++++++------ .../__tests__/configuration.test.ts | 20 +++++-- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/packages/html-to-lexical-parser/README.md b/packages/html-to-lexical-parser/README.md index 817b0fbcf30..27012d0bea5 100644 --- a/packages/html-to-lexical-parser/README.md +++ b/packages/html-to-lexical-parser/README.md @@ -7,18 +7,40 @@ ## About -This package provides method for parsing html to lexical object. +This package provides features that will enable you to parse your HTML pages into Lexical editor state object. + +Further, this lexical state object can be imported into Webiny's apps like the Page builder and Headless CMS, trough +the [Webiny's graphql API](https://www.webiny.com/docs/headless-cms/basics/graphql-api). + +> Webiny use the Lexical editor as primary rich text editor across the platform. + +Note: This module is built to be used in the `node.js` environment. + +#### About Lexical editor + +Lexical editor is product by Meta, provides rich text editing features, it's extensible and open source. In case you +are not familiar with the Lexical editor, please visit their official page +to [learn more](https://lexical.dev/docs/intro). ## Usage -To parse the html string, you need to import `parseHtmlToLexical` function, and provide -the html string. +To parse the html to lexical editor state object, you need to import `createHtmlToLexicalParser` factory function, +to create the parser function (with default or custom configuration) and provide the HTML content as parameter. +Parser will return Lexical editor state object. + +> The parser uses the default configuration with the Webiny's Lexical nodes. DOM elements like headings and +> paragraph, for example, will be converted to our custom Webiny Lexical nodes. ```tsx import {parseHtmlToLexical} from "@webiny/lexical-html-to-lexical-parser"; const htmlString = "

My paragraph

"; -const lexicalObject = parseHtmlToLexical(htmlString); + +// Create a parser function. +const myParser = createHtmlToLexicalParser(); + +// Parse the html string to Ledxical editor state object. +const lexicalEditorState = myParser(htmlString); ``` Here is the result in JSON format. This object structure is a valid Lexical editor state. @@ -56,7 +78,8 @@ Here is the result in JSON format. This object structure is a valid Lexical edit } ``` -Next, you can import the parsed Lexical JSON in the Headless CMS app. +Next, you can import the parsed Lexical JSON in our Headless CMS through the Graphql API. + To find more about how to use our GraphQl API please check our [GraphQL API Overview](https://www.webiny.com/docs/headless-cms/basics/graphql-api) article. @@ -78,8 +101,8 @@ const addCustomThemeStyleToHeadings = (node: LexicalNode): LexicalNode => { return node; }; -// App level configuration. -configureParser({ +// Create your parser with custom configuration +const myParser = createHtmlToLexicalParser({ // Lexical editor configuration editorConfig: { // Add custom nodes for parsing @@ -89,11 +112,12 @@ configureParser({ }, nodeMapper: addCustomThemeStyleToHeadings, normalizeTextNodes: true // by default is 'true' +}); -}) +const lexicalEditorState = myParser(htmlString); ``` -To learn more about how to create custom Lexical nodes or theme, please +To learn more about how to create custom Lexical nodes, please visit [Lexical's documentation web page](https://lexical.dev/docs/intro). ### Configuration options @@ -102,17 +126,13 @@ Configuration uses the `ParserConfigurationOptions` interface to define the conf By default, this parser configuration includes all lexical nodes from the @webiny/lexical-editor package. -| Prop | Type | Default value | Description | -|--------------------|---------------------------------------------------------|---------------|----------------------------------------------------------------------| -| editorConfig | CreateEditorArgs | { nodes: [] } | Configure the Lexical editor by providing the editor initialization | -| nodeMapper | ({ node: LexicalNode, editor: LexicalEditor } => void; | | Define custom mapper function for mapping lexical nodes. | -| normalizeTextNodes | boolean | true | By default, parser will wrap single text nodes with paragraph nodes. | +| Prop | Type | Default value | Description | +|--------------------|------------------------------------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| editorConfig | CreateEditorArgs | { nodes: [allWebinyNodes] } | Configure the Lexical editor by providing the native editor configuration options ([link to docs](https://lexical.dev/docs/api/modules/lexical#createeditorargs)) | +| nodeMapper | (node: LexicalNode, editor?: LexicalEditor) => LexicalNode | | Define custom mapper function to map the Lexical nodes. | +| normalizeTextNodes | boolean | true | By default, parser will normalize the nodes and prevent unsupported nodes to be inserted in the Lexical state. | -By providing the `editorConfig` configuration, we can add custom Lexical nodes, custom theme and other editor related -options. -Please check the full type definition, of the Lexical `CreateEditorArgs`, on the -following [link](https://lexical.dev/docs/api/modules/lexical#createeditorargs). diff --git a/packages/html-to-lexical-parser/__tests__/configuration.test.ts b/packages/html-to-lexical-parser/__tests__/configuration.test.ts index be3ce6ae6c4..1a49e6b768f 100644 --- a/packages/html-to-lexical-parser/__tests__/configuration.test.ts +++ b/packages/html-to-lexical-parser/__tests__/configuration.test.ts @@ -7,15 +7,27 @@ describe("Test how parser configuration options", () => { it("should be able to turn off default node text node normalizer", async () => { const parser = createHtmlToLexicalParser({ normalizeTextNodes: false }); - /** By removing the default text node normalizer, text nodes can't exist alone, so we expect error to be thrown. - * Error #56 on the lexical website: https://lexical.dev/docs/error?code=56 - */ + let editorState; try { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const nodes = parser(htmlWithNotSupportedHtmlTags); + editorState = parser(htmlWithNotSupportedHtmlTags); } catch (e) { + /** By removing the default text node normalizer, text nodes can't exist alone, so we expect error to be thrown. + * Error #56 on the lexical website: https://lexical.dev/docs/error?code=56 + */ expect(e.message).toMatch(e.message.contains("Minified Lexical error #56;")); } + + expect(editorState).toMatchObject({ + root: { + children: [], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); }); it("should be able to add custom node mapper", async () => { From 146969ba968df7c391e2411aa967a683b19879db Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Tue, 10 Oct 2023 13:37:20 +0200 Subject: [PATCH 21/31] wip: change the name of the package in readme file --- packages/html-to-lexical-parser/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/html-to-lexical-parser/README.md b/packages/html-to-lexical-parser/README.md index 27012d0bea5..03f40ad9852 100644 --- a/packages/html-to-lexical-parser/README.md +++ b/packages/html-to-lexical-parser/README.md @@ -1,7 +1,7 @@ -# @webiny/lexical-html-to-lexical-parser +# @webiny/html-to-lexical-parser -[![](https://img.shields.io/npm/dw/@webiny/lexical-html-to-lexical-parser.svg)](https://www.npmjs.com/package/@webiny/llexical-html-to-lexical-parser) -[![](https://img.shields.io/npm/v/@webiny/lexical-html-to-lexical-parser.svg)](https://www.npmjs.com/package/@webiny/lexical-html-to-lexical-parser) +[![](https://img.shields.io/npm/dw/@webiny/html-to-lexical-parser.svg)](https://www.npmjs.com/package/@webiny/llexical-html-to-lexical-parser) +[![](https://img.shields.io/npm/v/@webiny/html-to-lexical-parser.svg)](https://www.npmjs.com/package/@webiny/html-to-lexical-parser) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) From 7b284e1d64d688b2b692e4e7e342f2e927091257 Mon Sep 17 00:00:00 2001 From: Sasho Mihajlov Date: Thu, 12 Oct 2023 10:59:46 +0200 Subject: [PATCH 22/31] wip: remove the lexical editor param from node mapper function --- packages/html-to-lexical-parser/README.md | 10 +++++----- packages/html-to-lexical-parser/src/index.ts | 4 ++-- packages/html-to-lexical-parser/src/types.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/html-to-lexical-parser/README.md b/packages/html-to-lexical-parser/README.md index 03f40ad9852..b1b46c2ffb3 100644 --- a/packages/html-to-lexical-parser/README.md +++ b/packages/html-to-lexical-parser/README.md @@ -126,11 +126,11 @@ Configuration uses the `ParserConfigurationOptions` interface to define the conf By default, this parser configuration includes all lexical nodes from the @webiny/lexical-editor package. -| Prop | Type | Default value | Description | -|--------------------|------------------------------------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| editorConfig | CreateEditorArgs | { nodes: [allWebinyNodes] } | Configure the Lexical editor by providing the native editor configuration options ([link to docs](https://lexical.dev/docs/api/modules/lexical#createeditorargs)) | -| nodeMapper | (node: LexicalNode, editor?: LexicalEditor) => LexicalNode | | Define custom mapper function to map the Lexical nodes. | -| normalizeTextNodes | boolean | true | By default, parser will normalize the nodes and prevent unsupported nodes to be inserted in the Lexical state. | +| Prop | Type | Default value | Description | +|--------------------|------------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| editorConfig | CreateEditorArgs | { nodes: [allWebinyNodes] } | Configure the Lexical editor by providing the native editor configuration options ([link to docs](https://lexical.dev/docs/api/modules/lexical#createeditorargs)) | +| nodeMapper | (node: LexicalNode) => LexicalNode | | Define custom mapper function to map the Lexical nodes. | +| normalizeTextNodes | boolean | true | By default, parser will normalize the nodes and prevent unsupported nodes to be inserted in the Lexical state. | diff --git a/packages/html-to-lexical-parser/src/index.ts b/packages/html-to-lexical-parser/src/index.ts index c8276406b17..aa7fca217b4 100644 --- a/packages/html-to-lexical-parser/src/index.ts +++ b/packages/html-to-lexical-parser/src/index.ts @@ -53,8 +53,8 @@ export const createHtmlToLexicalParser = (config: ParserConfigurationOptions = { // Convert to lexical node objects that can be stored in db. const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document) - .map(node => textNodeNormalizer(node)) - .map(node => customNodeMapper(node, editor)); + .map(textNodeNormalizer) + .map(customNodeMapper); // Select the root $getRoot().select(); diff --git a/packages/html-to-lexical-parser/src/types.ts b/packages/html-to-lexical-parser/src/types.ts index b561682e575..9d7bcebcd05 100644 --- a/packages/html-to-lexical-parser/src/types.ts +++ b/packages/html-to-lexical-parser/src/types.ts @@ -1,6 +1,6 @@ -import { CreateEditorArgs, LexicalEditor, LexicalNode } from "lexical"; +import { CreateEditorArgs, LexicalNode } from "lexical"; -export type NodeMapper = (node: LexicalNode, editor?: LexicalEditor) => LexicalNode; +export type NodeMapper = (node: LexicalNode) => LexicalNode; export interface ParserConfigurationOptions { editorConfig?: CreateEditorArgs; From 53cd34a354a9d5dd0397fa7d39330b72d58f5ff4 Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Sun, 22 Oct 2023 16:21:34 +0200 Subject: [PATCH 23/31] chore: remove html-to-lexical-parser package --- packages/html-to-lexical-parser/README.md | 143 ------ .../__tests__/configuration.test.ts | 77 --- .../__tests__/html-to-lexical-nodes.test.ts | 456 ------------------ yarn.lock | 30 +- 4 files changed, 15 insertions(+), 691 deletions(-) delete mode 100644 packages/html-to-lexical-parser/README.md delete mode 100644 packages/html-to-lexical-parser/__tests__/configuration.test.ts delete mode 100644 packages/html-to-lexical-parser/__tests__/html-to-lexical-nodes.test.ts diff --git a/packages/html-to-lexical-parser/README.md b/packages/html-to-lexical-parser/README.md deleted file mode 100644 index b1b46c2ffb3..00000000000 --- a/packages/html-to-lexical-parser/README.md +++ /dev/null @@ -1,143 +0,0 @@ -# @webiny/html-to-lexical-parser - -[![](https://img.shields.io/npm/dw/@webiny/html-to-lexical-parser.svg)](https://www.npmjs.com/package/@webiny/llexical-html-to-lexical-parser) -[![](https://img.shields.io/npm/v/@webiny/html-to-lexical-parser.svg)](https://www.npmjs.com/package/@webiny/html-to-lexical-parser) -[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) - -## About - -This package provides features that will enable you to parse your HTML pages into Lexical editor state object. - -Further, this lexical state object can be imported into Webiny's apps like the Page builder and Headless CMS, trough -the [Webiny's graphql API](https://www.webiny.com/docs/headless-cms/basics/graphql-api). - -> Webiny use the Lexical editor as primary rich text editor across the platform. - -Note: This module is built to be used in the `node.js` environment. - -#### About Lexical editor - -Lexical editor is product by Meta, provides rich text editing features, it's extensible and open source. In case you -are not familiar with the Lexical editor, please visit their official page -to [learn more](https://lexical.dev/docs/intro). - -## Usage - -To parse the html to lexical editor state object, you need to import `createHtmlToLexicalParser` factory function, -to create the parser function (with default or custom configuration) and provide the HTML content as parameter. -Parser will return Lexical editor state object. - -> The parser uses the default configuration with the Webiny's Lexical nodes. DOM elements like headings and -> paragraph, for example, will be converted to our custom Webiny Lexical nodes. - -```tsx -import {parseHtmlToLexical} from "@webiny/lexical-html-to-lexical-parser"; - -const htmlString = "

My paragraph

"; - -// Create a parser function. -const myParser = createHtmlToLexicalParser(); - -// Parse the html string to Ledxical editor state object. -const lexicalEditorState = myParser(htmlString); -``` - -Here is the result in JSON format. This object structure is a valid Lexical editor state. - -```json -{ - "root": { - "children": [ - { - "children": [ - { - "detail": 0, - "format": 0, - "mode": "normal", - "style": "", - "text": "Space", - "type": "text", - "version": 1 - } - ], - "direction": null, - "format": "", - "indent": 0, - "styles": [], - "type": "paragraph-element", - "version": 1 - } - ], - "direction": null, - "format": "", - "indent": 0, - "type": "root", - "version": 1 - } -} -``` - -Next, you can import the parsed Lexical JSON in our Headless CMS through the Graphql API. - -To find more about how to use our GraphQl API please check -our [GraphQL API Overview](https://www.webiny.com/docs/headless-cms/basics/graphql-api) article. - -## Configuration - -To configure the parser import `configureParser` method and provide the app level configuration options. - -```ts -import {configureParser} from "@webiny/lexical-html-to-lexical-parser"; -import {myCustomTheme} from "./theme/myCustomTheme"; -import {MyCustomLexicalNode} from './lexical/nodes/MyCustomLexicalNode' - -const addCustomThemeStyleToHeadings = (node: LexicalNode): LexicalNode => { - if (node.getType() === "heading-element") { - return (node as HeadingNode).setThemeStyles([ - {styleId: "my-default-id", type: "typography"} - ]); - } - return node; -}; - -// Create your parser with custom configuration -const myParser = createHtmlToLexicalParser({ - // Lexical editor configuration - editorConfig: { - // Add custom nodes for parsing - nodes: [MyCustomLexicalNode], - // Add you custom theme - theme: myCustomTheme - }, - nodeMapper: addCustomThemeStyleToHeadings, - normalizeTextNodes: true // by default is 'true' -}); - -const lexicalEditorState = myParser(htmlString); -``` - -To learn more about how to create custom Lexical nodes, please -visit [Lexical's documentation web page](https://lexical.dev/docs/intro). - -### Configuration options - -Configuration uses the `ParserConfigurationOptions` interface to define the configuration options for the parser. - -By default, this parser configuration includes all lexical nodes from the @webiny/lexical-editor package. - -| Prop | Type | Default value | Description | -|--------------------|------------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| editorConfig | CreateEditorArgs | { nodes: [allWebinyNodes] } | Configure the Lexical editor by providing the native editor configuration options ([link to docs](https://lexical.dev/docs/api/modules/lexical#createeditorargs)) | -| nodeMapper | (node: LexicalNode) => LexicalNode | | Define custom mapper function to map the Lexical nodes. | -| normalizeTextNodes | boolean | true | By default, parser will normalize the nodes and prevent unsupported nodes to be inserted in the Lexical state. | - - - - - - - - - - diff --git a/packages/html-to-lexical-parser/__tests__/configuration.test.ts b/packages/html-to-lexical-parser/__tests__/configuration.test.ts deleted file mode 100644 index 1a49e6b768f..00000000000 --- a/packages/html-to-lexical-parser/__tests__/configuration.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { createHtmlToLexicalParser } from "~/index"; -import { headingH1Html, htmlWithNotSupportedHtmlTags } from "./test-data"; -import { LexicalNode } from "lexical"; -import { HeadingNode } from "@webiny/lexical-editor/nodes/HeadingNode"; - -describe("Test how parser configuration options", () => { - it("should be able to turn off default node text node normalizer", async () => { - const parser = createHtmlToLexicalParser({ normalizeTextNodes: false }); - - let editorState; - try { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - editorState = parser(htmlWithNotSupportedHtmlTags); - } catch (e) { - /** By removing the default text node normalizer, text nodes can't exist alone, so we expect error to be thrown. - * Error #56 on the lexical website: https://lexical.dev/docs/error?code=56 - */ - expect(e.message).toMatch(e.message.contains("Minified Lexical error #56;")); - } - - expect(editorState).toMatchObject({ - root: { - children: [], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); - }); - - it("should be able to add custom node mapper", async () => { - const addCustomThemeStyleToHeadings = (node: LexicalNode): LexicalNode => { - if (node.getType() === "heading-element") { - return (node as HeadingNode).setThemeStyles([ - { styleId: "my-default-id", type: "typography" } - ]); - } - return node; - }; - - const parser = createHtmlToLexicalParser({ nodeMapper: addCustomThemeStyleToHeadings }); - expect(parser(headingH1Html)).toMatchObject({ - root: { - children: [ - { - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "Testing heading h1 element", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - tag: "h1", - type: "heading-element", - version: 1, - // modified by node mapper - styles: [{ styleId: "my-default-id", type: "typography" }] - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); - }); -}); diff --git a/packages/html-to-lexical-parser/__tests__/html-to-lexical-nodes.test.ts b/packages/html-to-lexical-parser/__tests__/html-to-lexical-nodes.test.ts deleted file mode 100644 index 400ddcd7847..00000000000 --- a/packages/html-to-lexical-parser/__tests__/html-to-lexical-nodes.test.ts +++ /dev/null @@ -1,456 +0,0 @@ -/** - * @jest-environment jsdom - */ - -import { - boldItalicUnderlineFormatHtml, - bulletListHtml, - codeHtml, - headingH1Html, - headingH4Html, - htmlWithNotSupportedHtmlTags, - imageHtml, - linkHtml, - numberedListHtml, - paragraphHtmlTag, - quoteHtml -} from "./test-data"; -import { createHtmlToLexicalParser } from "../src/index"; - -const parser = createHtmlToLexicalParser(); - -describe("Test how parser convert the html tags into webiny's lexical node objects", () => { - it("should parse html to paragraph node", async () => { - expect(parser(paragraphHtmlTag)).toMatchObject({ - root: { - children: [ - { - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "Testing paragraph element", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - styles: [], - type: "paragraph-element", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); - }); -}); -it("should normalize the text node - wrap the node in paragraph element", () => { - /** - * By default, parser will normalize the nodes and convert DOM nodes to supported lexical nodes. - * It's expected all unsupported html tags like div, figure, button and other to be removed and not converted. - */ - expect(parser(htmlWithNotSupportedHtmlTags)).toMatchObject({ - root: { - children: [ - { - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "See all 37 photos", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - styles: [], - type: "paragraph-element", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); -}); - -it("should generate paragraph element with text node with bold, italic and underline formats", () => { - expect(parser(boldItalicUnderlineFormatHtml)).toMatchObject({ - root: { - children: [ - { - children: [ - { - detail: 0, - format: 11, // all formats - mode: "normal", - style: "", - text: "formatted text", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "paragraph-element", - version: 1, - styles: [] - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); -}); - -it("should generate heading nodes", () => { - /** - * Test HTML h1 tag - */ - expect(parser(headingH1Html)).toMatchObject({ - root: { - children: [ - { - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "Testing heading h1 element", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - tag: "h1", - type: "heading-element", - version: 1, - styles: [] - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); - - /** - * Test HTML h4 tag - */ - expect(parser(headingH4Html)).toMatchObject({ - root: { - children: [ - { - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "Testing heading h4 element", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - tag: "h4", - type: "heading-element", - version: 1, - styles: [] - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); -}); - -it("should generate bullet list, and nested list item nodes", () => { - expect(parser(bulletListHtml)).toMatchObject({ - root: { - children: [ - { - children: [ - { - checked: undefined, - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "list item 1", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "webiny-listitem", - version: 1, - value: 1 - }, - { - checked: undefined, - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "list item 2", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "webiny-listitem", - version: 1, - value: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "webiny-list", - version: 1, - listType: "bullet", - start: 1, - tag: "ul", - themeStyleId: "" - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); -}); - -it("should generate numbered list, and nested list item nodes", () => { - expect(parser(numberedListHtml)).toMatchObject({ - root: { - children: [ - { - children: [ - { - checked: undefined, - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "list item 1", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "webiny-listitem", - version: 1, - value: 1 - }, - { - checked: undefined, - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "list item 2", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "webiny-listitem", - version: 1, - value: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "webiny-list", - version: 1, - listType: "number", - start: 1, - tag: "ol", - themeStyleId: "" - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); -}); - -it("should not generate paragraph and link node", () => { - expect(parser(linkHtml)).toMatchObject({ - root: { - children: [ - { - children: [ - { - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "My webiny link", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "link-node", - version: 1, - rel: "noopener noreferrer", - target: "_blank", - title: null, - url: "https://webiny.com" - } - ], - direction: null, - format: "", - indent: 0, - type: "paragraph-element", - version: 1, - styles: [] - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); -}); - -it("should not generate image node", () => { - expect(parser(imageHtml)).toMatchObject({ - root: { - children: [], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); -}); - -it("should generate quote node", () => { - expect(parser(quoteHtml)).toMatchObject({ - root: { - children: [ - { - children: [ - { - detail: 0, - format: 0, - mode: "normal", - style: "", - text: "My quote block", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "webiny-quote", - version: 1, - styles: [], - styleId: undefined - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); -}); - -it("should generate code node", () => { - expect(parser(codeHtml)).toMatchObject({ - root: { - children: [ - { - children: [ - { - detail: 0, - format: 16, // lexical number for code format - mode: "normal", - style: "", - text: "Text code formatting", - type: "text", - version: 1 - } - ], - direction: null, - format: "", - indent: 0, - type: "paragraph-element", - version: 1, - styles: [] - } - ], - direction: null, - format: "", - indent: 0, - type: "root", - version: 1 - } - }); -}); diff --git a/yarn.lock b/yarn.lock index eec2661db0d..763a1de999f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16179,21 +16179,6 @@ __metadata: languageName: unknown linkType: soft -"@webiny/html-to-lexical-parser@workspace:packages/html-to-lexical-parser": - version: 0.0.0-use.local - resolution: "@webiny/html-to-lexical-parser@workspace:packages/html-to-lexical-parser" - dependencies: - "@lexical/headless": ^0.11.3 - "@lexical/html": ^0.11.3 - "@types/jsdom": 21.1.3 - "@webiny/cli": 0.0.0 - "@webiny/lexical-editor": 0.0.0 - "@webiny/project-utils": 0.0.0 - jsdom: 22.1.0 - lexical: ^0.11.3 - languageName: unknown - linkType: soft - "@webiny/i18n-react@0.0.0, @webiny/i18n-react@workspace:packages/i18n-react": version: 0.0.0-use.local resolution: "@webiny/i18n-react@workspace:packages/i18n-react" @@ -16257,6 +16242,21 @@ __metadata: languageName: unknown linkType: soft +"@webiny/lexical-converter@workspace:packages/lexical-converter": + version: 0.0.0-use.local + resolution: "@webiny/lexical-converter@workspace:packages/lexical-converter" + dependencies: + "@lexical/headless": ^0.11.3 + "@lexical/html": ^0.11.3 + "@types/jsdom": 21.1.3 + "@webiny/cli": 0.0.0 + "@webiny/lexical-editor": 0.0.0 + "@webiny/project-utils": 0.0.0 + jsdom: 22.1.0 + lexical: ^0.11.3 + languageName: unknown + linkType: soft + "@webiny/lexical-editor-actions@0.0.0, @webiny/lexical-editor-actions@workspace:packages/lexical-editor-actions": version: 0.0.0-use.local resolution: "@webiny/lexical-editor-actions@workspace:packages/lexical-editor-actions" From 8bb3b93a233e90a98b9e13e77a98db4827441997 Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Sun, 22 Oct 2023 16:21:58 +0200 Subject: [PATCH 24/31] feat(lexical-converter): introduce a lexical-converter package --- .../.babelrc.js | 2 +- .../LICENSE | 0 packages/lexical-converter/README.md | 106 ++++ .../__tests__/htmlToLexicalState.test.ts | 514 ++++++++++++++++++ .../__tests__/mocks/htmlMocks.ts} | 31 +- .../__tests__/mocks/stateMocks.ts | 278 ++++++++++ .../__tests__/setup}/setupEnv.ts | 0 .../__tests__/stateTransformer.test.ts | 148 +++++ .../__tests__/utils/toDom.ts | 34 ++ .../jest.config.js | 4 +- .../package.json | 6 +- .../src/createHtmlToLexicalParser.ts} | 33 +- .../src/createLexicalStateTransformer.ts | 83 +++ packages/lexical-converter/src/index.ts | 2 + .../src/types.ts | 2 +- .../tsconfig.build.json | 0 .../tsconfig.json | 0 .../webiny.config.js | 0 18 files changed, 1220 insertions(+), 23 deletions(-) rename packages/{html-to-lexical-parser => lexical-converter}/.babelrc.js (65%) rename packages/{html-to-lexical-parser => lexical-converter}/LICENSE (100%) create mode 100644 packages/lexical-converter/README.md create mode 100644 packages/lexical-converter/__tests__/htmlToLexicalState.test.ts rename packages/{html-to-lexical-parser/__tests__/test-data.ts => lexical-converter/__tests__/mocks/htmlMocks.ts} (62%) create mode 100644 packages/lexical-converter/__tests__/mocks/stateMocks.ts rename packages/{html-to-lexical-parser/__tests__ => lexical-converter/__tests__/setup}/setupEnv.ts (100%) create mode 100644 packages/lexical-converter/__tests__/stateTransformer.test.ts create mode 100644 packages/lexical-converter/__tests__/utils/toDom.ts rename packages/{html-to-lexical-parser => lexical-converter}/jest.config.js (61%) rename packages/{html-to-lexical-parser => lexical-converter}/package.json (83%) rename packages/{html-to-lexical-parser/src/index.ts => lexical-converter/src/createHtmlToLexicalParser.ts} (75%) create mode 100644 packages/lexical-converter/src/createLexicalStateTransformer.ts create mode 100644 packages/lexical-converter/src/index.ts rename packages/{html-to-lexical-parser => lexical-converter}/src/types.ts (78%) rename packages/{html-to-lexical-parser => lexical-converter}/tsconfig.build.json (100%) rename packages/{html-to-lexical-parser => lexical-converter}/tsconfig.json (100%) rename packages/{html-to-lexical-parser => lexical-converter}/webiny.config.js (100%) diff --git a/packages/html-to-lexical-parser/.babelrc.js b/packages/lexical-converter/.babelrc.js similarity index 65% rename from packages/html-to-lexical-parser/.babelrc.js rename to packages/lexical-converter/.babelrc.js index bec58b263bd..9da7674cb52 100644 --- a/packages/html-to-lexical-parser/.babelrc.js +++ b/packages/lexical-converter/.babelrc.js @@ -1 +1 @@ -module.exports = require("@webiny/project-utils").createBabelConfigForReact({ path: __dirname }); +module.exports = require("@webiny/project-utils").createBabelConfigForNode({ path: __dirname }); diff --git a/packages/html-to-lexical-parser/LICENSE b/packages/lexical-converter/LICENSE similarity index 100% rename from packages/html-to-lexical-parser/LICENSE rename to packages/lexical-converter/LICENSE diff --git a/packages/lexical-converter/README.md b/packages/lexical-converter/README.md new file mode 100644 index 00000000000..cffe210173f --- /dev/null +++ b/packages/lexical-converter/README.md @@ -0,0 +1,106 @@ +# @webiny/lexical-converter + +[![](https://img.shields.io/npm/dw/@webiny/lexical-converter.svg)](https://www.npmjs.com/package/@webiny/llexical-lexical-converter) +[![](https://img.shields.io/npm/v/@webiny/lexical-converter.svg)](https://www.npmjs.com/package/@webiny/lexical-converter) +[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) + +## About + +This package provides features that will enable you to parse your HTML pages into Lexical editor state object. + +Further, this lexical state object can be imported into Webiny's apps like the Page builder and Headless CMS, trough the [Webiny's graphql API](https://www.webiny.com/docs/headless-cms/basics/graphql-api). + +> Webiny use the Lexical editor as primary rich text editor across the platform. + +Note: This module is built to be used in the `node.js` environment. + +## Usage + +To parse the HTML to lexical editor state object, you need to import `createHtmlToLexicalParser` factory function, +to create the parser function (with default or custom configuration) and provide the HTML content as parameter. +Parser will return Lexical editor state object. + +> The parser uses the default configuration with the Webiny's Lexical nodes. DOM elements like headings and +> paragraph, for example, will be converted to our custom Webiny Lexical nodes. + +```tsx +import { createHtmlToLexicalParser } from "@webiny/lexical-converter"; + +const htmlString = "

My paragraph

"; + +// Create a parser function. +const myParser = createHtmlToLexicalParser(); + +// Parse the HTML string to Lexical editor state object. +const lexicalEditorState = myParser(htmlString); +``` + +Here is the result in JSON format. This object structure is a valid Lexical editor state. + +```json +{ + "root": { + "children": [ + { + "children": [ + { + "detail": 0, + "format": 0, + "mode": "normal", + "style": "", + "text": "Space", + "type": "text", + "version": 1 + } + ], + "direction": null, + "format": "", + "indent": 0, + "styles": [], + "type": "paragraph-element", + "version": 1 + } + ], + "direction": null, + "format": "", + "indent": 0, + "type": "root", + "version": 1 + } +} +``` + +## Configuration + +To configure the parser, you can pass an optional configuration object to the parser factory. + +```ts +import { createHtmlToLexicalParser } from "@webiny/lexical-converter"; +import { myCustomTheme } from "./theme/myCustomTheme"; +import { MyCustomLexicalNode } from "./lexical/nodes/MyCustomLexicalNode"; + +const addCustomThemeStyleToHeadings = (node: LexicalNode): LexicalNode => { + if (node.getType() === "heading-element") { + return (node as HeadingNode).setThemeStyles([{ styleId: "my-default-id", type: "typography" }]); + } + return node; +}; + +// Create your parser with custom configuration +const myParser = createHtmlToLexicalParser({ + // Lexical editor configuration + editorConfig: { + // Add custom nodes for parsing + nodes: [MyCustomLexicalNode], + // Add you custom theme + theme: myCustomTheme + }, + nodeMapper: addCustomThemeStyleToHeadings, + normalizeTextNodes: false // Default: true +}); + +const lexicalEditorState = myParser(htmlString); +``` + +To learn more about how to create custom Lexical nodes, please visit [Lexical's documentation web page](https://lexical.dev/docs/intro). diff --git a/packages/lexical-converter/__tests__/htmlToLexicalState.test.ts b/packages/lexical-converter/__tests__/htmlToLexicalState.test.ts new file mode 100644 index 00000000000..a96d64b486a --- /dev/null +++ b/packages/lexical-converter/__tests__/htmlToLexicalState.test.ts @@ -0,0 +1,514 @@ +/** + * @jest-environment jsdom + */ +import { createMocks } from "./mocks/htmlMocks"; +import { createHtmlToLexicalParser } from "~/index"; +import { toBrowserDom, toJsDom } from "./utils/toDom"; +import { $isHeadingNode } from "@webiny/lexical-editor"; +import { LexicalNode } from "lexical"; + +const defaultParser = createHtmlToLexicalParser(); + +describe("HTML to Lexical State Parser", () => { + // We're testing 2 parsers: JSOM and + const jsDomMocks = createMocks(toJsDom); + const domParserMocks = createMocks(toBrowserDom); + + [jsDomMocks, domParserMocks].forEach(mocks => { + describe(`Test parsing with "${mocks.domParser}"`, () => { + it("should parse html to paragraph node", async () => { + expect(defaultParser(mocks.paragraphHtmlTag)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "Testing paragraph element", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + styles: [], + type: "paragraph-element", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); + + it("should normalize the text node - wrap the node in paragraph element", () => { + /** + * By default, parser will normalize the nodes and convert DOM nodes to supported lexical nodes. + * It's expected all unsupported html tags like div, figure, button and other to be removed and not converted. + */ + expect(defaultParser(mocks.htmlWithUnsupportedHtmlTags)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "See all 37 photos", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + styles: [], + type: "paragraph-element", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); + + it("should generate paragraph element with text node with bold, italic and underline formats", () => { + expect(defaultParser(mocks.boldItalicUnderlineFormatHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 11, // all formats + mode: "normal", + style: "", + text: "formatted text", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "paragraph-element", + version: 1, + styles: [] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); + + it("should generate heading nodes", () => { + /** + * Test HTML h1 tag + */ + expect(defaultParser(mocks.headingH1Html)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "Testing heading h1 element", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + tag: "h1", + type: "heading-element", + version: 1, + styles: [] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + + /** + * Test HTML h4 tag + */ + expect(defaultParser(mocks.headingH4Html)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "Testing heading h4 element", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + tag: "h4", + type: "heading-element", + version: 1, + styles: [] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); + + it("should generate bullet list, and nested list item nodes", () => { + expect(defaultParser(mocks.bulletListHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + checked: undefined, + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "list item 1", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-listitem", + version: 1, + value: 1 + }, + { + checked: undefined, + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "list item 2", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-listitem", + version: 1, + value: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-list", + version: 1, + listType: "bullet", + start: 1, + tag: "ul", + themeStyleId: "" + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); + + it("should generate numbered list, and nested list item nodes", () => { + expect(defaultParser(mocks.numberedListHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + checked: undefined, + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "list item 1", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-listitem", + version: 1, + value: 1 + }, + { + checked: undefined, + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "list item 2", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-listitem", + version: 1, + value: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-list", + version: 1, + listType: "number", + start: 1, + tag: "ol", + themeStyleId: "" + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); + + it("should not generate paragraph and link node", () => { + expect(defaultParser(mocks.linkHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "My webiny link", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "link-node", + version: 1, + rel: "noopener noreferrer", + target: "_blank", + title: null, + url: "https://webiny.com" + } + ], + direction: null, + format: "", + indent: 0, + type: "paragraph-element", + version: 1, + styles: [] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); + + it("should not generate image node", () => { + expect(defaultParser(mocks.imageHtml)).toMatchObject({ + root: { + children: [], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); + + it("should generate quote node", () => { + expect(defaultParser(mocks.quoteHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "My quote block", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "webiny-quote", + version: 1, + styles: [], + styleId: undefined + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); + + it("should generate code node", () => { + expect(defaultParser(mocks.codeHtml)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 16, // lexical number for code format + mode: "normal", + style: "", + text: "Text code formatting", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + type: "paragraph-element", + version: 1, + styles: [] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); + + it("should turn off node text normalization", async () => { + const parser = createHtmlToLexicalParser({ normalizeTextNodes: false }); + const messages = [ + "rootNode.append: Only element or decorator nodes can be appended to the root node", + "Minified Lexical error #56" + ]; + + expect(() => parser(mocks.htmlWithUnsupportedHtmlTags)).toThrow( + process.env.NODE_ENV === "development" ? messages[0] : messages[1] + ); + }); + + it("should be able to add custom node mapper", async () => { + const addCustomThemeStyleToHeadings = (node: LexicalNode): LexicalNode => { + if ($isHeadingNode(node)) { + return node.setThemeStyles([ + { styleId: "my-default-id", type: "typography" } + ]); + } + return node; + }; + + const parser = createHtmlToLexicalParser({ + nodeMapper: addCustomThemeStyleToHeadings + }); + expect(parser(mocks.headingH1Html)).toMatchObject({ + root: { + children: [ + { + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: "Testing heading h1 element", + type: "text", + version: 1 + } + ], + direction: null, + format: "", + indent: 0, + tag: "h1", + type: "heading-element", + version: 1, + // modified by node mapper + styles: [{ styleId: "my-default-id", type: "typography" }] + } + ], + direction: null, + format: "", + indent: 0, + type: "root", + version: 1 + } + }); + }); + }); + }); +}); diff --git a/packages/html-to-lexical-parser/__tests__/test-data.ts b/packages/lexical-converter/__tests__/mocks/htmlMocks.ts similarity index 62% rename from packages/html-to-lexical-parser/__tests__/test-data.ts rename to packages/lexical-converter/__tests__/mocks/htmlMocks.ts index 4642c6cd6be..6c928254134 100644 --- a/packages/html-to-lexical-parser/__tests__/test-data.ts +++ b/packages/lexical-converter/__tests__/mocks/htmlMocks.ts @@ -1,5 +1,7 @@ +import { DomFactory } from "../toDom"; + export const paragraphHtmlTag = `

Testing paragraph element

`; -export const htmlWithNotSupportedHtmlTags = `
+export const htmlWithUnsupportedHtmlTags = `