Closed de83c78d1f closed 4 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}
/>
);
Note that the argument to
text
renderer is astring
– 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>
@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.
@drschulz I only could make this work if my React components in the markdown file are inline :(
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.
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} />
}
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.
Would be amazing if react-markdown supported custom components in a way similar to markdown-to-jsx which, unfortunately, is no longer actively maintained.
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>
const renderers: ReactMarkdown.Renderers = { myNodeType: (props: string) => (
which is written in markdown as:
@TacticalSlothmaster
I couldn't get this to work. It turns into < >. any clue?
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.
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
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?
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.
is renderer no longer available as a prop?
Please see changelog: https://github.com/remarkjs/react-markdown/blob/main/changelog.md#600---2021-04-15.
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 thosecolons
? If it's possible - it's a bit unclear how to useplugins
orrenderers
to make it work. I tried to use naive approach:It actually renders one emoji and deletes everything else.