remarkjs / react-markdown

Markdown component for React
https://remarkjs.github.io/react-markdown/
MIT License
13.23k stars 874 forks source link

Completely Custom Component #218

Closed de83c78d1f closed 4 years ago

de83c78d1f commented 6 years ago

Is there a possibility to render completely custom component? For example I want to parse for emojis and add emoji-mart component on match. I expect to render something like <Emoji emoji={colons} set="apple" size={20} /> on match - so where do i get those colons? If it's possible - it's a bit unclear how to use plugins or renderers to make it work. I tried to use naive approach:

<ReactMarkdown
  source={text}
  renderers={{
    link: props => <a href={props.href} target="_blank">{props.children}</a>,
    text: ({source}: ReactMarkdownProps) => {
      return source!.match(/(:[a-zA-Z0-9-_+]+:)/g) ? <Emoji emoji={source!} set="apple" size={20} /> : source;
    }
  }}
 />

It actually renders one emoji and deletes everything else.

brookback commented 6 years ago

Note that the argument to text renderer is a string – not an object. I'm using this:

import Emoji from 'react-emoji-render';
import * as ReactMarkdown from 'react-markdown';

const renderers: ReactMarkdown.Renderers = {
    text: (props: string) => (
        <Emoji text={props} />
    ),
};

const Text = ({ text, className }) => (
    <ReactMarkdown
        source={text}
        renderers={renderers}
    />
);
TacticalSlothmaster commented 6 years ago

Note that the argument to text renderer is a string – not an object. I'm using this:

import Emoji from 'react-emoji-render';
import * as ReactMarkdown from 'react-markdown';

const renderers: ReactMarkdown.Renderers = {
    text: (props: string) => (
        <Emoji text={props} />
    ),
};

const Text = ({ text, className }) => (
    <ReactMarkdown
        source={text}
        renderers={renderers}
    />
);

Is it possible to create an own custom node type together with those custom components? E.g.:

`const renderers: ReactMarkdown.Renderers = { myNodeType: (props: string) => (

),

};`

which is written in markdown as:

<myNodeType>some Text</myNodeType>

drschulz commented 5 years ago

@TacticalSlothmaster So I've been doing some experimenting and found a way to put jsx into our markdown, although it requires another dependency to actually parse the jsx. Using https://github.com/TroyAlford/react-jsx-parser seems like a good option.

The main goal is to trick the markdown parser into thinking that your jsx is just html. To do this you can just wrap your entire snippet in a tag without a prop (can even just call it React). This is needed because anytime a tag has an object prop at the top level (or any non string prop), the markdown parser parses the line as text. Custom html tags are allowed in the github markdown spec: https://github.github.com/gfm/#example-133. When parsed as html, the entire snippet is grabbed, so we'll have our entire jsx context to pass to a jsx dedicated parser.

Example:

Markdown file/text:

# Title
Here is some data:

<React>
  <MyChart size={300} type='pie' />
</React>

In your code:

import MyChart from "./MyChart";
import JsxParser from "react-jsx-parser";
import ReactMarkdown from "react-markdown";

const componentTransforms = {
  React: (props) => <>{props.children</>,
  MyChart
};

const renderers: ReactMarkdown.Renderers = {
  html: (props) => <JsxParser jsx={props.value} components={componentTransforms} />
};

const Text = ({ text, className }) => (
  <ReactMarkdown source={text} renderers={renderers} />
);

Should work for any of your components as long as you add them to the componentTransforms object. That react-jsx-parser also parses plain html so your html inside the markdown should still render as expected.

richardaum commented 5 years ago

@drschulz I only could make this work if my React components in the markdown file are inline :(

konsumer commented 5 years ago

You could also combine this idea with code-blocks. That way you can tell parser "this is some other kinda thing", which might be especially good for doc-sites (similar to how styleguidist works), but could be pretty workable for regular stuff, too. I made an example here.

basically:

# title here

Some regular text

* a
* list
* of
* things

```react
<MyChart size={300} type='pie' />
```

I really like that things that should be rendered different go in code-fences. You can use a similar concept for any language in your code block, and it will still look right on github (like here):

# title

```emoji
Specially processed stuff, use react in your custom renderer to handle it.
```

which could be rendered with this:

const renderers = {
  code: ({ language, value }) => {
    if (language === 'emoji') {
      return <Emoji text={value} />
    }
    const className = language && `language-${language}`
    const code = React.createElement('code', className ? { className: className } : null, value)
    return React.createElement('pre', {}, code)
  }
}

This method doesn't require any separate JSX parsing or exposing react components, it just uses the language to trigger how it should deal with the text inside the fence.

konsumer commented 5 years ago

You can also abuse another tag you don't use too often, like blockquote:

> Some emoji-processed text
const renderers = {
  blockquote: ({ children }) => <Emoji text={children} />
}
drschulz commented 5 years ago

Oh wow yeah that code block method with ```react is even nicer than the html way because you are ensured to get text as the value which is perfect for the jsx parser. Definitely less room for errors. Also love the scoped component examples, could be super helpful for doc writers without react knowledge.

sunknudsen commented 4 years ago

Would be amazing if react-markdown supported custom components in a way similar to markdown-to-jsx which, unfortunately, is no longer actively maintained.

See https://github.com/probablyup/markdown-to-jsx#optionsoverrides---rendering-arbitrary-react-components

sunknudsen commented 4 years ago

I personally crave this feature to feed the following pseudo-html to a custom YouTubePlayer component.

<youtubeplayer title="Why Macs equipped with the new T2 chip are shitty for hackers" url="https://www.youtube.com/watch?v=brGLX_92F5o">YouTube player placeholder</youtubeplayer>

markdown-to-jsx handles this elegantly, but the markdown parser has issues (see https://github.com/probablyup/markdown-to-jsx/issues/289 and https://github.com/probablyup/markdown-to-jsx/issues/292).

<MarkdownToJSX
  options={{
    overrides: {
      youtubeplayer: {
        component: YouTubePlayer,
      },
    },
  }}
>
  {props.story.content}
</MarkdownToJSX>
samupra commented 4 years ago

const renderers: ReactMarkdown.Renderers = { myNodeType: (props: string) => ( ), };

which is written in markdown as:

some Text

@TacticalSlothmaster

I couldn't get this to work. It turns into &lt &gt. any clue?

joa-queen commented 4 years ago

You can create a plugin that adds custom types to nodes. For example:

import visit from 'unist-util-visit';

const EMOJI_RE = /:\+1:|:-1:|:[\w-]+:/;

const extractText = (string, start, end) => {
  const startLine = string.slice(0, start).split('\n');
  const endLine = string.slice(0, end).split('\n');

  return {
    type: 'text',
    value: string.slice(start, end),
    position: {
      start: {
        line: startLine.length,
        column: startLine[startLine.length - 1].length + 1,
      },
      end: {
        line: endLine.length,
        column: endLine[endLine.length - 1].length + 1,
      },
    },
  };
};

const plugin = () => {
  function transformer(tree) {
    visit(tree, 'text', (node, position, parent) => {
      const definition = [];
      let lastIndex = 0;
      let match;

      while ((match = EMOJI_RE.exec(node.value)) !== null) {
        const value = match[0];
        const type = 'emoji';

        if (match.index !== lastIndex) {
          definition.push(extractText(node.value, lastIndex, match.index));
        }

        definition.push({
          type,
          value,
        });

        lastIndex = match.index + value.length;
      }

      if (lastIndex !== node.value.length) {
        const text = extractText(node.value, lastIndex, node.value.length);
        definition.push(text);
      }

      const last = parent.children.slice(position + 1);
      parent.children = parent.children.slice(0, position);
      parent.children = parent.children.concat(definition);
      parent.children = parent.children.concat(last);
    });
  }

  return transformer;
};

export default plugin;

And then create a renderer for your ReactMarkdown, like so:

<ReactMarkdown
  source={content}
  plugins={[yourplugin]}
  renderers={{
    emoji: ({ value }) => (
      <EmojiMart
        emoji={value}
        size={20}
        set="apple"
        fallback={(e, p) => (e ? `:${e.short_names[0]}:` : p.emoji)}
      />
    )
  }}
/>

Hope it helps.

ChristianMurphy commented 4 years ago

It is possible as others mention through plugins/parser extensions. You can find more information on creating plugins at https://unifiedjs.com/learn

More directly you may be interested in react-markdown's sibling project, MDX https://github.com/mdx-js/mdx Which has direct support for JSX, including custom elements inside markdown. https://mdxjs.com

akhils220 commented 3 years ago

Which is the best possible approach to render JSX inside ReactMarkdown ? Do we need to use "react-jsx-parser" or are there any other methods?

wooorm commented 3 years ago

That’s off topic: this question is about other things.

For the answer, that’s what MDX does: you can use https://github.com/wooorm/xdm or https://github.com/mdx-js/mdx.

brunobraga95 commented 3 years ago

is renderer no longer available as a prop?

wooorm commented 3 years ago

Please see changelog: https://github.com/remarkjs/react-markdown/blob/main/changelog.md#600---2021-04-15.