rich-text icon indicating copy to clipboard operation
rich-text copied to clipboard

Missing converted line breaks in rich-text-html-renderer

Open xyNNN opened this issue 6 years ago • 17 comments

When I'm using the rich-text-html-renderer I expect that also the line breaks are converted to HTML
also. Currently when I've some manual line breaks in a paragraph they are ignored completely.

Line1
Line2

should be converted to

<p>
Line1<br />
Line2
</p>

xyNNN avatar May 25 '19 13:05 xyNNN

Hey @xyNNN, line breaks stored as new line separators within a text node.

"Before publishing your content, you can preview it using the Content Preview A\nPI."

However, unlike react-renderer, html-renderer does not expose renderText method to control the presentation of the text nodes.

The same behaviour could be achieved by hooking into renderNode renderer e.g.:

renderNode: {
    [BLOCKS.PARAGRAPH]: (node, next) => next(node.content).replace('\n', '<br/>')
  }

sbezludny avatar May 27 '19 05:05 sbezludny

Thanks @sbezludny for your fast reply!

Sure this should be a way to solve it. But shouldn't the richt-text-html-renderer convert per default the \n to
?

xyNNN avatar May 27 '19 09:05 xyNNN

I understand the frustration, but that would require a breaking change. I will put a label for now and get back to this issue before releasing a next major version.

sbezludny avatar May 28 '19 07:05 sbezludny

Make sense and thank you very much. I'm looking forward to the next major release.

xyNNN avatar May 28 '19 08:05 xyNNN

Thank you, @sbezludny for your code snipped above. It helped us allot See

renderNode: {
    [BLOCKS.PARAGRAPH]: (node, next) => next(node.content).replace('\n', '<br/>')
  }

I enhenced it a little bit, because your example stripped away the <p /> and only converted the first \n' into <br />

Maybe this is helpful for others as well

renderNode: {
  [BLOCKS.PARAGRAPH]: (node, next) => `<p>${next(node.content).replace(/\n/g, '<br/>')}</p>`,
}

aalibaabaa avatar Jun 27 '19 08:06 aalibaabaa

If I am currently using:

    const Text = ({ children }) => <p>{children}</p>

    const options = {
      renderNode: {
        [BLOCKS.PARAGRAPH]: (node, children) => <Text>{children}</Text>,
      },
    }

and my JSX looks like this: {documentToReactComponents(this.props.data.allContentfulContactPage.edges[0].node.address.json, options)}

How do I incorporate this change so that I get my line breaks? I am not sure what "next" is or why you guys are using it. I have been following the docs from Contentful but I am having trouble. I used to use a plugin that allowed you to query the html but that is now deprecated and I am having a hard time with this.

Thanks

quinnmyers avatar Jul 11 '19 06:07 quinnmyers

@quinnmyers The above comments and examples are mainly referring to using rich-text-html-renderer, not rich-text-react-renderer. That's why these examples use next rather than children.

The documentation for rich-text-react-renderer actually gives an example for how to use the renderText option to replace \n line breaks with <br />.

Try using this options definition:

const options = {
  renderText: text => text.split('\n').flatMap((text, i) => [i > 0 && <br />, text])
}

You can see a working example on codesandbox.io.

DanweDE avatar Jul 13 '19 08:07 DanweDE

It is absolutely ridiculous why contentful makes it so hard to write content on the platform. Very frustrated

arhoy avatar Apr 08 '20 00:04 arhoy

@arhoy Do you have any constructive feedback? We have thousands of users producing content each day, so not sure what exactly you find ridiculous.

DanweDE avatar Apr 09 '20 09:04 DanweDE

I'm using @DanweDE 's above solution in @contentful/rich-text-react-renderer at 13.4.0.

In Safari and Firefox, my content looks fine.

In Chrome, my line breaks are appearing as a strange missing character:

image

Turns out the culprit is U+2028 (Line separator).

See Chromium bug: https://bugs.chromium.org/p/chromium/issues/detail?id=550275

My hacky solution is:

renderText: (text) =>
  text
    .replace(/\u2028/g, "")
    .split("\n")
    .flatMap((text, i) => [i > 0 && <br />, text])

My constructive feedback ;) would be to make it easy and simple to configure either the RTE and/or Content Model and/or rich-text-react-renderer to display line breaks as break elements. Either through some UI checkbox or some options key like renderNewlinesAsBreaks: true. Or all of the above.

Hope this helps someone.

stephenhmarsh avatar May 26 '20 00:05 stephenhmarsh

I had the problem that flatMap is not supported in my deployment. This is my hacky alternative that worked for me:

renderText: text =>
    text.split('\n').map((t, i) =>
      i > 0 ? (
        <React.Fragment key={`${i}-${t. slice(0, 5)}`}>
          <br />
          {t}
        </React.Fragment>
      ) : (
        t
      ),
    ),

Edit: using React.Fragment from the comment below

schoenwaldnils avatar Jun 01 '20 10:06 schoenwaldnils

Building off of @schoenwaldnils 's pure map() answer: I was getting linter warnings about React requiring unique keys for each child.

Simple fix:

  renderText: (text: string) =>
    text.split('\n').map((t, i) =>
      i > 0 ? (
        <React.Fragment key={i}>
          <br />
          {t}
        </React.Fragment>
      ) : (
        t
      ),
    ),

cjimmy avatar Feb 23 '21 03:02 cjimmy

I think I had the same problem with the linter. Mine also warned about the index in the key: react/no-array-index-key

<React.Fragment key={`${i}-${t. slice(0, 5)}`}>

This way the key should be pretty unique across the DOM

schoenwaldnils avatar Feb 23 '21 16:02 schoenwaldnils

Just a heads-up, we are using some contentful content to pre-populate rich text editor field using quill. The problem with blindly replacing all \n with <br/> is that some rich text editors will replace two <br/> elements in a row with <p><br/></p> which looks wrong. In our case we really want to treat two <br/> elements in a row as a new paragraph. The function below does that conversion - hope it helps someone else.

const ALL_NEWLINES = /\n/g;
const DOUBLE_NEWLINES = /\n\n/g;
const END_TAG = /(<\/[^>]+>)$/;
const START_TAG = /^(<[^>]+>)/;

const matchingGroup = (match: RegExpMatchArray | null): string => (match?.length && match[1]) || '';

export const asHtml = (document: Document): string =>
    documentToHtmlString(document, {
        renderNode: {
            [BLOCKS.PARAGRAPH]: (node, next) => {
                const text = next(node.content);
                const startTag = matchingGroup(text.match(START_TAG));
                const endTag = matchingGroup(text.match(END_TAG));
                return `<p>${text
                    .replace(DOUBLE_NEWLINES, `${endTag}</p><p>${startTag}`)
                    .replace(ALL_NEWLINES, '<br/>')}</p>`;
            },
        },
    });

Here is a test that does a few things but highlights the output (could be broken up I guess)

    describe('asHtml', () => {
        it('should return html content with double newlines replaced by paragraph breaks (preserving existing end tags) and single newlines replaced by <br/>', () => {
            expect(asHtml(CONTENTFUL_DOCUMENT_WITH_NEWLINES)).toBe(
                '<p><b>Line 1</b></p><p><b><br/>Line2<br/>Line3</b></p><p><i>Paragraph2</i></p>',
            );
        });
    });


   ...

const CONTENTFUL_DOCUMENT_WITH_NEWLINES: Document = {
    nodeType: BLOCKS.DOCUMENT,
    data: {},
    content: [
        {
            nodeType: BLOCKS.PARAGRAPH,
            data: {},
            content: [
                {
                    nodeType: 'text',
                    value: 'Line 1\n\n\nLine2\nLine3',
                    marks: [{ type: 'bold' }],
                    data: {},
                },
            ],
        },
        {
            nodeType: BLOCKS.PARAGRAPH,
            data: {},
            content: [
                {
                    nodeType: 'text',
                    value: 'Paragraph2',
                    marks: [{ type: 'italic' }],
                    data: {},
                },
            ],
        },
    ],
};

mbyrne00 avatar Aug 22 '21 23:08 mbyrne00

Hey there,

for me works an ordinary white-space: pre-line; on the <p> which renders line breaks perfectly without adding <br>s to the DOM.

Like:

const Text = ({ children }) => (
  <p style={{ whiteSpace: 'pre-line' }}>{children}</p>
)

const renderOptions = {
  renderNode: {
    [BLOCKS.PARAGRAPH]: (node, children) => <Text>{children}</Text>,
  },
}

danielbeutner avatar Oct 28 '21 10:10 danielbeutner

If you want to replace the new line with something very specific, you can use this method:

        [BLOCKS.PARAGRAPH]: (node: any, children: any) => {
            return <p>{
                children.map((line: string | any) => {
                    if (!line) {
                        return <br/>;
                    } else {
                        return line;
                    }
                })
            }</p>;
        },

For me all the solutions above didn't work since I'm using tailwindcss and it's reseting certain styles for empty paragraphs.

14h avatar Sep 17 '22 11:09 14h

If you want to replace the new line with something very specific, you can use this method:

        [BLOCKS.PARAGRAPH]: (node: any, children: any) => {
            return <p>{
                children.map((line: string | any) => {
                    if (!line) {
                        return <br/>;
                    } else {
                        return line;
                    }
                })
            }</p>;
        },

For me all the solutions above didn't work since I'm using tailwindcss and it's reseting certain styles for empty paragraphs.

Could you use whitespace-pre-line (https://tailwindcss.com/docs/whitespace) here? Honestly I am not using Tailwind but I wonder if this works.

danielbeutner avatar Sep 17 '22 11:09 danielbeutner

Having line breaks should be the default or at least an option so we dont need to call string.replace() for each text on each render call...

MickL avatar Apr 13 '23 12:04 MickL

My workaround for this was treating empty paragraph blocks as <br />. Leaving incase it helps anyone!

renderText: (text) => {
    if (!text) return <br />; // <-- THIS
    return text.split("\n").flatMap((text, i) => [i > 0 && <br />, text]);
  },

andenacitelli avatar May 06 '23 14:05 andenacitelli

I dont know if this is still an issue or not, but one solution worked brilliantly for me atleast without any hassle is,

renderMark: {
    [MARKS.CODE]: (text) => ( <pre><code>{text}</code></pre> ),
}, 

And this works with rich-text-react-renderer itself.

Cnerd-Mahadi avatar Jul 19 '23 07:07 Cnerd-Mahadi

Yep - all solutions depend on how the content is being used. In my case I wanted unstyled plain old HTML so that I could put snippets together how I want, use in emails, use elsewhere ... and approach styling independently. The HTML was just wrong before. If others don't have that concern then styling is an easier approach.

mbyrne00 avatar Jul 20 '23 23:07 mbyrne00

I dont know if this is still an issue or not, but one solution worked brilliantly for me atleast without any hassle is,

renderMark: {
    [MARKS.CODE]: (text) => ( <pre><code>{text}</code></pre> ),
}, 

And this works with rich-text-react-renderer itself.

This causes

Warning: validateDOMNesting(...): <pre> cannot appear as a descendant of <p>.

I used the below instead.

renderMark: {
[MARKS.CODE]: (text) => (
        <span style={{ whiteSpace: 'break-spaces' }}><code>{text}</code></span>
    ),
}

matskohe avatar Nov 06 '23 21:11 matskohe

My workaround for this was treating empty paragraph blocks as <br />. Leaving incase it helps anyone!

renderText: (text) => {
    if (!text) return <br />; // <-- THIS
    return text.split("\n").flatMap((text, i) => [i > 0 && <br />, text]);
  },

This creates an extra <br /> before and after hyperlink.

I use the below.

renderNode: {
  [BLOCKS.PARAGRAPH]: (node, children) => {
    if (
      node.nodeType === "paragraph" &&
      node.content[0].nodeType === "text" &&
      node.content[0].value === "" &&
       // prevent hyperlink from becoming <br />
       !(node.content[1]?.nodeType === "hyperlink")
    ) return <br />;
    return <p>{children}</p>;
  },
},

matskohe avatar Nov 06 '23 23:11 matskohe