如何用React组件替换Markdown呈现的HTML标记?

时间:2018-01-07 18:55:29

标签: reactjs markdown

目前,我正在使用marked将Markdown转换为HTML代码,然后用React元素替换它的某些部分。这会产生一个 HTML字符串数组和React元素,它们可以实际呈现:

const prepareGuide = markdown => replaceToArray(
  marked(markdown),
  /<a href="SOME_SPECIAL_HREF".*?>(.*?)<\/a>/,
  (match, label, slug) => <a href={`/${slug}`}>{label}</a>
)

const Guide = ({ guide }) =>
  prepareGuide(guide.fields.text).map(
    n => typeof n === 'object'
      ? n
      : <span dangerouslySetInnerHTML={{ __html: n }} />
  )

这个问题,让我们称之为解决方法,就是每个HTML都需要一个包装元素,比如span(并使用dangerouslySetInnerHTML)。

我基本上需要的是能够用React组件替换渲染的HTML元素,以添加React功能,如路由器链接和其他自定义元素。

还有其他方法吗?

编辑:我使用的replaceToArray函数类似于String.prototype.replace,但返回一个数组(因此可以返回任何类型)

编辑:我的另一种方法是将HTML直接呈现给DOM(使用dangerouslySetInnerHTML),并使用容器元素的ref来查询我要替换的所有元素。但是,接下来的问题:要在我的HTML ref中呈现React组件,我需要另一个React root,这是可能的,但不实用,因为我会丢失所有的上下文(比如Router),所以我甚至都不能正确使用路由器链接。

2 个答案:

答案 0 :(得分:0)

我能够解决这个问题如下:

我一直使用markeddangerouslySetInnerHTML来直接设置HTML。现在,如第二种方法所述,我使用ref来查询我想要替换的元素。现在,为了能够将React元素呈现给HTML,我只使用了ReactDOM.render函数。

最大的问题是组件无法访问应用程序的上下文,因为我现在有多个React根。为了解决这个问题,我发现我们可以将上下文从一个组件复制到另一个组件:Is it possible to pass context into a component instantiated with ReactDOM.render?

因此,为了能够访问呈现HTML的组件中的上下文,我们需要为我们需要复制的上下文设置组件contextTypes

class Article extends Component {
  static contextTypes = {
    router: PropTypes.any
  }

  static propTypes = {
    markdown: PropTypes.string
  }

  prepare(ref) {
    const Provider = createContextProvider(this.context)
    const links = Array.from(ref.querySelectorAll('a'))

    links.forEach((link) => {
      const span = document.createElement('span')
      const { pathname } = url.parse(link.href)
      const text = link.innerText

      link.parentNode.replaceChild(span, link)
      ReactDOM.render(
        <Provider>
          <Link to={pathname}>{text}</Link>
        </Provider>,
        span
      )
    })
  }

  render() {
    return (
      <article
        ref={this.prepare}
        dangerouslySetInnerHTML={{ __html: marked(this.props.markdown) }}
      />
    )
  }
}

上面的代码需要我从上面链接的问题中复制的剪辑。我调用prepare的方法用React根替换特定的HTML节点。

function createContextProvider(context) {
  class ContextProvider extends React.Component {
    getChildContext() {
      return context
    }

    render = () => this.props.children

    static propTypes = { children: PropTypes.node }
  }

  ContextProvider.childContextTypes = {}
  Object.keys(context).forEach(key => {
    ContextProvider.childContextTypes[key] = PropTypes.any.isRequired
  })

  return ContextProvider
}

所以我们基本上有一个创建Provider组件的函数。该函数需要能够动态地适应所需的上下文类型,这就是循环将其设置为必需的原因。

答案 1 :(得分:0)

如果您只想让链接与React Router一起使用,则可以像往常一样使用危险的SetInnerHTML呈现降价,然后拦截内部链接点击,使它们通过react-路由器。

完整示例,从外部加载.md,然后捕获链接以使用react-router进行处理:

import React from "react"
import { withRouter } from 'react-router'

import catchLinks from 'catch-links'
import marked from "marked"

import styles from './styles.scss'

class ContentPage extends React.Component {

    constructor(props) {
        super(props);
        this.state = {
            loading: false,
            markdown: '',
            title: ''
        }
    }

    componentDidMount() {

        if (typeof window !== 'undefined') {
            catchLinks(window, (href) => {
                this.props.history.push(href);
            });
        }

        const page = location.pathname.replace('/', '');

        fetch(`/assets/content/${page}.md`)
        .then(response => response.text())
        .then(text => {
            this.setState({ markdown: marked(text, {}) })
        });

        const title = page_titles[page] || capitalize(page);

        if (title) {
            document.title = title;
            this.setState({title})
        }

    }

    render() {

        const {
            markdown,
            title
        } = this.state;

        return (
            <div class={styles.contentPage}>

                <div class={styles.pageTop}>
                    {title}
                </div>

                <div class={styles.page}>
                    <div class={styles.content} dangerouslySetInnerHTML={{__html: markdown}}></div>
                </div>

            </div>
        );
    }
}

export default withRouter(ContentPage);