contentful / rich-text

Libraries for handling and rendering Rich Text 📄
MIT License
549 stars 110 forks source link

Rendering code snippets with Gatsby #105

Open sebge-1 opened 5 years ago

sebge-1 commented 5 years ago

I'm trying to include code snippets into my rich text document and render it properly formatted later on by wrapping <pre> tags around what Gatsby generates. I tried doing this:


const Blog = (props) => {
  const Code = ({children}) => <pre><code style={{backgroundColor: "red"}}>{children}</code></pre>
  const options = {
   renderMark: {
      [MARKS.CODE]: code => <Code>{code}</Code>},
  }
}

This doesn't work though because a new <p> tag is automatically wrapped around every line of code in the rich text document, which creates invalid HTML (<pre> tags can't be direct descendants of <p> tags) and separates each line from the others, rather than displaying the code as a block.

Am I going about this the wrong way? All I want is to include a block of code in e.g a blog post and render it in my template exactly as I typed it out in Contentful.

Would appreciate any help! Thanks

sbezludny commented 5 years ago

Hey @scirocco21 at the moment we do not natively support code blocks in the Rich Text field.

As a workaround, you can use Embedded Entry. A dedicated content type will allow specifying code metadata, like: language etc., that could be used at rendering time.

Hope this helps ;)

dillonstreator commented 4 years ago

@sbezludny Is this still unavailable?

As a workaround, you can use Embedded Entry. A dedicated content type will allow specifying code metadata, like: language etc., that could be used at rendering time.

Do you have an example of how we would go about implementing this workaround?

DanweDE commented 4 years ago

Hi @DillonStreator, Sadly, builtin code blocks are still not a feature.

You can look at the rich-text-react-renderer usage section which provides an example for rendering BLOCKS.EMBEDDED_ENTRY. In your case, you would need to check if the provided node.data.target.sys.contentType.sys.id equals the content type you designate as code block content type and render as <pre/> accordingly. This code block content type should have a code text field representing your code snippet which should be accessible as node.data.target.fields.code.

johnkavanagh commented 3 years ago

I believe there are a few problems at play here. Instead of trying to write a huge comment (although inevitably this will still be quite big), I've written a detailed article about it here.

First and foremost, the suggestion from @sbezludny that Contentful does not natively support code blocks in the Rich Text field is - please pardon my French - BS. For such a feature-rich - and expensive - service, I'm sure we can agree that's not really acceptable when you consider that code bocks are made available within the Contentful interface. I would perhaps understand a little more if formatted code blocks also weren't available there.. but it is, so being able to enter content that it is then not possible to render out again is a fairly significant flaw.

Screenshot 2021-03-01 at 09 31 35

Secondly, what @scirocco21 was trying to do was a little mistaken. [MARKS.CODE] is inline code (as in: code that falls within the flow of a paragraph), not a code block. These should be wrapped in <code> and not <pre> (for what it's worth: both of those tags are formatted as inline code). For reference, MARKS also contains BOLD, ITALIC, and UNDERLINE.

Finally, Contentful does not provide a 'code' block-type, which I think it an oversight on their part. What this means is that when you create a block of code in Contentful, what you are actually creating is paragraphs, with the only child inside being a 'code' MARK. This - I feel - is where Contentful is really letting themselves down at the moment.

To get around this, you can modify how you handle BLOCKS.PARAGRAPH. We already know that where we've entered a block of text into Contentful's UI, what it has sent us is a paragraph, with a single node (normal text paragraphs will likely contain several nodes for inline elements within it), and the mark type of that first node will be 'code'.

So, when we come to render a paragraph, instead of just outputting a <p>, we delve a little deeper to work out if it's a code block or not, and return <pre> (and if you want: <code>) instead:

[BLOCKS.PARAGRAPH]: (node, children) => {
  if (
    node.content.length === 1 &&
    find(node.content[0].marks, { type: 'code' })
  ) {
    return <pre><code>{node.content[0].value}</code></pre>;
  }

  return <p>{children}</p>;
}

This will (very belatedly) resolve the original issue here, although it's a workaround. There is one minor hitch - however - that that's this: because Contentful separates these code blocks out into paragraphs first, if your code has an empty line within it, you will actually find it gets divided out into multiple <pre> sections.

Here's a screenshot from my personal website showing you what I mean (each is wrapped in a lime outline):

Screenshot 2021-03-01 at 10 55 09

This can be countered visually with a little bit of CSS to close the gaps, but nevertheless: what you end up with isn't semantic markup, and has other issues like odd line-wrapping and horizontal scrolling for narrower screens where you end up being forced to use white-space: pre-wrap, which isn't ideal.

With the help of a close friend of mine (and very talented developer) Ben Stokoe, we have come up with a way of transforming Contentful's data and bringing these adjacent code blocks together.

This comment is already a bit long to start dropping multi-line functions into, so if the above solution isn't enough, then take a read through my article 'Rendering Contentful Rich Conde snippets in Gatsby' - the bigger solution is towards the bottom.

ImranSefat commented 3 years ago

I'm using TailwindCSS in a NEXT JS Project. The details object is the RichText field type coming from API request from Contentful. Document to React Component package didn't work to display inline assets such as images. That's why I'm using @madebyconnor/rich-text-to-jsx. Cheers! 🔥

import { MARKS } from '@contentful/rich-text-types';
import RichText from '@madebyconnor/rich-text-to-jsx';

<RichText
        richText={details} //details is the richtext field type coming from contentful
        overrides={{
           [MARKS.CODE]: (node) => {
               return <div className="px-6 py-3 my-4 bg-gray-500 text-blue-300 font-mono rounded-lg">{node.children}</div>
               }
        }}
/>
coreymunn3 commented 3 years ago

I ended up solving this in an odd way actually, though some of the comments above are really helpful.

I chose to render the CODE marks inside pre tags with pre-wrap set to maintain spacing as shown below. When I typed out my code, instead of hitting enter to create newlines, I hit shift + enter which allowed me to type code on a newline without inserting a newline character into the markup. The result was a single big block of code without unnecessary white spacing, formatted how I intended as per CodeBlock styling.

[MARKS.CODE]: (text) => (
        <CodeBlock>
            <Text as='pre' whiteSpace='pre-wrap'>
              {text}
            </Text>
        </CodeBlock>
      ),
defjosiah commented 2 years ago

I also ran into this issue, but with an embedded-entry-block

See https://github.com/contentful/rich-text/issues/153#issuecomment-1038426911 for the workaround.

I really think that this should be handled by the library, since it's a common use-case, and the workarounds are all pretty similar (evaluate the content for children, and properly handle the nesting).

We're evaluating contentful in a trial phase and this is a fairly major issue to workaround.

ramialkaro commented 1 year ago

Thanks to @johnkavanagh to give the hint of the solution. Did tweak it little bit because I could not find the "find" function in the solution. I use the react-code-blocks https://www.npmjs.com/package/react-code-blocks to show the code snippet

Live example: https://posts.ramialkaro.fi/posts/mastering-javascript-or-8-string-methods-or-part-4

import { documentToReactComponents } from '@contentful/rich-text-react-renderer'
import { BLOCKS  } from '@contentful/rich-text-types'
import markdownStyles from './markdown-styles.module.css'
import RichTextAsset from './rich-text-asset'
import { CopyBlock,  dracula } from "react-code-blocks";

const find = (array, condition) => {
  return array.find(item => condition(item));
};

const customMarkdownOptions = (content) => ({
  renderNode: {
    [BLOCKS.EMBEDDED_ASSET]: (node) => (
      <RichTextAsset
        id={node.data.target.sys.id}
        assets={content.links.assets.block}
      />
    ),
    [BLOCKS.PARAGRAPH]: (node, children) => {
      if (find(node.content[0].marks, mark => mark.type === 'code')) {
        return <CopyBlock
        text={node.content[0].value}
        language={"jsx"}
        showLineNumbers={true}
        wrapLines={true}
        codeBlock
        theme={dracula}
      />
      }

      return <p>{children}</p>;
    }
  },
});

export default function PostBody({ content }) {
  return (
    <div className="max-w-2xl mx-auto">
      <div className={markdownStyles['markdown']}>
        {documentToReactComponents(
          content.json,
          customMarkdownOptions(content)
        )}
      </div>
    </div>
  )
}

image