payload icon indicating copy to clipboard operation
payload copied to clipboard

Preserve indent and text-align when converting Lexical <-> HTML

Open wkentdag opened this issue 1 year ago • 13 comments

Link to reproduction

https://github.com/wkentdag/payload/blob/main/test/_community/int.spec.ts#L80-L100

Describe the Bug

I followed the recommended approach for converting lexical rich text into HTML, and discovered that some of the formatting gets stripped in the process. Inline marks, headings, and lists transfer fine; but indentation, alignment, and preformatted whitespace are ignored.

I realize I may need to set up my own Feature to handle preformatted whitespace, but I expect that the align and indent features will work out-of-the-box.

To demonstrate, things look correct in the editor:

Screen Shot 2024-02-21 at 1 05 03 PM

But when I convert the Lexical markup to HTML, it looks like this:

Screen Shot 2024-02-21 at 1 04 45 PM

In my test repro I am using the "ad-hoc" method of converting Lexical to HTML, but you get the same result with the HTMLConverterFeature. Happy to add another test case with that example, I just skipped it to keep things simple.

To Reproduce

  1. Clone my fork
  2. run jest test/_community/int.spec.ts -t '_Community Tests correctly formats rich text'
  3. Note the failing test, which specifically chokes on the center-aligned and indented paragraphs.

Payload Version

2.11.1

Adapters and Plugins

db-mongodb

wkentdag avatar Feb 21 '24 21:02 wkentdag

+1, setting align does nothing in the HTML for me when using HTMLConverterFeature

tjmills-dev avatar Mar 11 '24 15:03 tjmills-dev

There is no Converter for align and indent. But "whitespace stripped" is not bug. Just add CSS white-space.

SimYunSup avatar Apr 02 '24 00:04 SimYunSup

@SimYunSup fair, but I expect the generated HTML to match the markup rendered (correctly) in the admin Lexical editor out of the box. so IMO this is still a bug.

One possible solution would be to add <pre> to the default feature set. Seems like a simple enough feature, I'd be happy to make a PR for that. what do you think @AlessioGr ?

wkentdag avatar Apr 10 '24 01:04 wkentdag

I think adding align and indent to the default converters would be a good idea. PRs for that are welcome!

I do think that we shouldn't output hard-coded style attributes in the generated HTML, though. Feels too opinionated. But we can add classNames that make it easy for the user to target those with their own styles

AlessioGr avatar Apr 10 '24 03:04 AlessioGr

@AlessioGr I wasn't suggesting modifying the generated HTML, but rather adding a preformatted whitespace feature that you can select from the dropdown. So where I'm currently expecting this snippet of rendered HTML:

<p>weirdly formatted</p>

I'd swap the paragraph for pre and get this instead:

<pre>weirdly formatted</pre>.

That's an enhancement that I'm planning to tackle. However, my primary concern is that the indent and align features are stripped during HTML conversion. Is that not a bug?

wkentdag avatar Apr 10 '24 04:04 wkentdag

The Lexical Adapter is creating a Feature by adding Nodes and Plugins. It's important to note that paragraph (as <p> tag) has a Node and a corresponding converter, while Align and Indent only add attributes to the Node(code), doesn't have a Node.

If you want to convert a <p> tag to a <pre> tag, you can change the paragraph converter from HTMLConverterFeature in your service code.

However, Align and Indent are different. They are not Node-agonistic features, so you need to add a new converter (type HTMLConverter)

SimYunSup avatar Apr 10 '24 10:04 SimYunSup

Thanks for the explanation @SimYunSup, I'm definitely still trying to wrap my head around all the Lexical concepts.

I'm able to reproduce align and indent in HTML by tweaking the default paragraph converter like this:

export const FormattedParagraphHTMLConverter: HTMLConverter<any> = {
  // @ts-expect-error figure out what submissionData is/does
  async converter({ converters, node, parent, submissionData }) {
    const childrenText = await convertLexicalNodesToHTML({
      converters,
      lexicalNodes: node.children,
      parent: {
        ...node,
        parent,
      },
      // @ts-expect-error saa
      submissionData,
    })

    let pTag = '<p>'
    let style = ''
    if (!!node.format) {
      style += `text-align: ${node.format};`
    }

    if (node.indent > 0) {
      style += `text-indent:${node.indent}em;`
    }

    if (!!style) {
      pTag = `<p style="${style}">`
    }

    return `${pTag}${childrenText}</p>`
  },
  nodeTypes: ['paragraph'],
}

It would be nice to have something like this in the default config without having to setup a custom feature. I can see how hardcoding the style attributes is a little rigid...what kind of interface are you imagining for providing classNames @AlessioGr ? Or is it just as simple as swapping class="align-center" for style="text-align:center;" ?

wkentdag avatar Apr 12 '24 07:04 wkentdag

I agree that aligning and indentations should be resolved by default in the serializer. Will this solution be merged anytime?

ozzestrophic avatar Jul 16 '24 13:07 ozzestrophic

I also would like for this PR (https://github.com/payloadcms/payload/pull/5814) to be merged / reviewed. Feels like extremely basic (and desirable) functionality for those who want to render lexical to HTML. Otherwise you have no other way of positioning text based on the editors AlignFeature. Looks like it pops up on the Discord community-help quite frequently too.

Is there a way to implement the PR locally as a workaround? The convertLexicalNodesToHTML from payload/packages/richtext-lexical/src/field/features/converters/html/converter/index.ts isn't exported which makes this annoying.

mobeigi avatar Sep 01 '24 01:09 mobeigi

@mobeigi convertLexicalNodesToHTML is exported in @payloadcms/richtext-lexical.

https://github.com/payloadcms/payload/blob/25e9bc62dbcbabcb3619cf83e3dc0110e0a4cabf/packages/richtext-lexical/src/index.ts#L276-L279

But you should use it with the following caution.

It must be head of converter because find converter with Array.find.

SimYunSup avatar Sep 01 '24 14:09 SimYunSup

@wkentdag Hi, i'd like to use your code in my project, by creating a utils and import it like so:

return [
          ...rootFeatures,
          ...defaultFeatures,
          HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
          FixedToolbarFeature(),
          InlineToolbarFeature(),
          HTMLConverterFeature({}),
          FormattedParagraphHTMLConverter()
        ]

but it gives error: FormattedParagraphHTMLConverter) is not a function

erwin-gapai avatar Sep 08 '24 09:09 erwin-gapai

Status: @SimYunSup kindly offered to contribute to this issue.

The current PR addressing this is https://github.com/payloadcms/payload/pull/8030

GermanJablo avatar Sep 17 '24 14:09 GermanJablo

Status update 2

I ended up opening the following PR to resolve the indent issue here: https://github.com/facebook/lexical/pull/6693

I'll do the same for text-align shortly.

If anyone is interested, contributions are still welcome for HTML serialization to include custom indentation values in editorConfig.theme.indent, as explained in the linked PR.

GermanJablo avatar Oct 03 '24 00:10 GermanJablo

Temporary solution using patch-package inspired by @wkentdag that do not require any change in the project code.

diff --git a/dist/features/converters/html/converter/converters/paragraph.js b/dist/features/converters/html/converter/converters/paragraph.js
index e6b2305e6a12a3006989ee47fb29073a6d8d3e55..ba4179bcad9f01e484509a2fff376348f41e69f7 100644
--- a/dist/features/converters/html/converter/converters/paragraph.js
+++ b/dist/features/converters/html/converter/converters/paragraph.js
@@ -1,6 +1,6 @@
 import { convertLexicalNodesToHTML } from '../index.js';
 export const ParagraphHTMLConverter = {
-    async converter ({ converters, node, parent, req }) {
+    async converter({ converters, node, parent, req }) {
         const childrenText = await convertLexicalNodesToHTML({
             converters,
             lexicalNodes: node.children,
@@ -10,7 +10,22 @@ export const ParagraphHTMLConverter = {
             },
             req
         });
-        return `<p>${childrenText}</p>`;
+
+        let pTag = '<p>'
+        let style = ''
+        if (!!node.format) {
+            style += `text-align: ${node.format};`
+        }
+
+        if (node.indent > 0) {
+            style += `padding-inline-start:${node.indent * 40}px;`
+        }
+
+        if (!!style) {
+            pTag = `<p style="${style}">`
+        }
+
+        return `${pTag}${childrenText}</p>`
     },
     nodeTypes: [
         'paragraph'

MohammadKurjieh avatar Nov 06 '24 17:11 MohammadKurjieh

🚀 This is included in version v3.1.0

github-actions[bot] avatar Nov 22 '24 17:11 github-actions[bot]

This issue has been automatically locked. Please open a new issue if this issue persists with any additional detail.

github-actions[bot] avatar Nov 24 '24 04:11 github-actions[bot]