plotly / dash-core-components

OBSOLETE: now part of https://github.com/plotly/dash
https://dash.plotly.com
MIT License
271 stars 145 forks source link

[Feature Request] Markdown Plugins #929

Open karosc opened 3 years ago

karosc commented 3 years ago

Is there any interest in supporting remarkjs plugins? I am currently using dcc.Markdown a bunch and would really benefit from some extended functionality, like being able to add ids and classes using the curly brace syntax to help me style content.

alexcjohnson commented 3 years ago

(moved to the DCC repo)

That would be great! We'll have be careful to do it in a way that doesn't bloat the component unless they're used. Individually plugins generally seem pretty small but there are a lot of them we might include. I could imagine a number of ways this might be done, from requiring users to load plugins manually on the page, to one or many async chunks defined within this repo that get loaded on demand. Regardless, the dash prop would need to reference the plugins as strings, which then get associated with the correct code inside the component.

karosc commented 3 years ago

I'm not a react developer (or a js developer for that matter), but interested in seeing if I can clobber something together. On the react markdown readme, it seems strait forward to include a plugin:

const React = require('react')
const ReactMarkdown = require('react-markdown')
const render = require('react-dom').render
const gfm = require('remark-gfm')

const markdown = `Just a link: https://reactjs.com.`

render(<ReactMarkdown plugins={[gfm]} children={markdown} />, document.body)

could we add a method to the dash markdown class that looks something like this?

load_plugins(text_array){
        let plugins = [];
        if(text_array.includes('remark-attr')){
            const attr_plugin = require('remark-attr');
            plugins.push(attr_plugin);
        }

        return plugins
    }

Then push the results of that function to the Markdown jsx in the render function like this


    render() {
        const {
            id,
            style,
            className,
            highlight_config,
            loading_state,
            dangerously_allow_html,
            children,
            dedent,
            plugins,  //new plugin prop
        } = this.props;

        const textProp =
            type(children) === 'Array' ? children.join('\n') : children;
        const displayText =
            dedent && textProp ? this.dedent(textProp) : textProp;

        const plugs = this.load_plugins(plugins)   //load plugins given list of strings

        const componentTransforms = {
            dccLink: props => <DccLink {...props} />,
            dccMarkdown: props => (
                <Markdown
                    {...mergeDeepRight(
                        pick(['dangerously_allow_html', 'dedent','plugins'], this.props),  // not sure what this line does, but the dedent is added, so wondering if plugings belongs here to?
                        pick(['children'], props)
                    )}
                />
            ),
        };

        return (
            <div
                id={id}
                ref={node => {
                    this.mdContainer = node;
                }}
                style={style}
                className={
                    ((highlight_config && highlight_config.theme) ||
                        className) &&
                    `${className ? className : ''} ${
                        highlight_config &&
                        highlight_config.theme &&
                        highlight_config.theme === 'dark'
                            ? 'hljs-dark'
                            : ''
                    }`
                }
                data-dash-is-loading={
                    (loading_state && loading_state.is_loading) || undefined
                }
            >
                <Markdown
                    source={displayText}
                    escapeHtml={!dangerously_allow_html}
                    plugins = {plugs}   // add in plugin list
                    renderers={{
                        html: props =>
                            props.escapeHtml ? (
                                props.value
                            ) : (
                                <JsxParser
                                    jsx={props.value}
                                    components={componentTransforms}
                                    renderInWrapper={false}
                                />
                            ),
                    }}
                />
            </div>
        );
    }
karosc commented 3 years ago

You know, my main use case for these plugins would be solved by allowing html to rendered from the markdown document. However, I am hesitant to use dangerously_allow_html because I don't fully understand the risks and don't want to expose users to such a threat.

I found a different repository based on remarkjs (react-remark) that seems to allow for html sanitization using rehype plugins, which might improve the security of using dangerously_allow_html.

What are your thoughts of porting the dcc.Markdown component from react-markdown to react-remark. Doing so would increase the opportunity to use more plugins that operate both on markdown and html, and potentially increase security against XSS.