實(shí)現(xiàn)Markdown的React組件

準(zhǔn)備工作

在項(xiàng)目中安裝以下依賴:

  • "rehype-raw": "^5.1.0"
  • "remark-gfm": "^1.0.0"
  • "remark-parse": "^9.0.0"
  • "remark-rehype": "^8.1.0"
  • "unified": "^9.0.0"

工作目標(biāo)

組件接受Markdow語法的字符串,將其轉(zhuǎn)移為React組件

工作內(nèi)容

編寫md字符串轉(zhuǎn)移為HTML AST樹的方法

import React from 'react'
import unified from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import remarkGfm from 'remark-gfm'

export function mdTextToHTMLAst(text: string): Promise<RootNode> {
  return new Promise(resolve => {
    // FIXME: 這邊 this 的實(shí)際類型為 Processor<void, Input, Input, Output>朗儒,
    // 但是改為實(shí)際類型比較麻煩吗垮,所以先 as any 了
    function getHTMLAstPlugin(this: any) {
      Object.assign(this, { Compiler: compiler })
      function compiler(root: RootNode) {
        resolve(root)
      }
    }

    unified()
      .use(remarkParse) // md text -> md ast
      .use(remarkGfm)  // 解決非CommonMark語法不能解析的問題
      .use(remarkRehype) // md ast -> html ast
      .use(getHTMLAstPlugin)
      .process(text)
  })
}

將HTML AST樹轉(zhuǎn)為React的虛擬DOM

export type RootNode = {
  type: 'root'
  children: Array<TextNode | ElementNode>
}

type TextNode = {
  type: 'text'
  value: string
}

type ElementNode = {
  type: 'element'
  children: Array<TextNode | ElementNode>
  properties: object
  tagName: keyof ReactHTML
}

function childrenToReactNode(children: Array<TextNode | ElementNode>,parent?: ElementNode | RootNode) {
  children = [...children]
  const res: ReactNode[] = []
  let key = 0
  for (let i = 0; i < children.length; i++) {
    const current = children[i]

    if (current.type === 'text') {
      const text = renderTextNode(current, parent)
      if (text !== null) {
        res.push(text)
      }
      continue
    }

    if (current.type === 'element') {
      res.push(renderElementNode(current, key++))
      continue
    }
  }
  return res
}

function renderTextNode(child: TextNode, parent?: ElementNode | RootNode) {
  if (
    child.value === '\n' ||
    (parent && parent.type === 'element' && tableElements.has(parent.tagName))
  ) {
    // 去除不必要的空白文本,React does not permit whitespace text elements as children of table
    return null
  }
  return child.value
}

function renderElementNode(element: ElementNode,key: number): ReactNode {
  const children = element.children
  const len = children.length
  const tagName = element.tagName.toLowerCase()

  if (tagName === 'style' || tagName === 'script') {
    return null
  }

  const reactElement = React.createElement(
    tagName,
    { key, ...element.properties, style: undefined, className: undefined },
    len !== 0 ? childrenToReactNode(children, element) : null
  )

  if (tagName === 'table') {
    // 表格外面包一層 div,防止寬度超出
    return <div key={key}>{reactElement}</div>
  }

  return reactElement
}

繪制HTML AST樹

export function renderHTMLAst(htmlAst: RootNode) {
  return React.createElement(
    Fragment,
    null,
    childrenToReactNode(htmlAst.children)
  )
}

制作一個(gè)React組件用來展示HTML AST樹

export function HtmlAst(props: { htmlAst: RootNode; className?: string }) {
  const { htmlAst, className } = props

  const element = useMemo(() => renderHTMLAst(htmlAst), [htmlAst])

  return (
    <>
      {/** 這邊之所以使用自定義標(biāo)簽,是為了保證這邊樣式的獨(dú)立性(不會(huì)影響到其他頁(yè)面)之外,
       * 又降低了優(yōu)先級(jí)(防止覆蓋渲染 md 所替換的組件里面的樣式) */}
      {React.createElement('markdown-container', { class: className }, element)}
    </>
  )
}

制作一個(gè)React組件作為封裝的最外層

function Markdown(props: { text: string; className?: string }) {
  const { text, className } = props
  const [articleHtmlAst, setArticleHtmlAst] = useState<RootNode | null>(null)

  useEffect(() => {
    mdTextToHTMLAst(text).then(res => setArticleHtmlAst(res))
  }, [text])

  if (articleHtmlAst == null) {
    return <></>
  }

  return <HtmlAst htmlAst={articleHtmlAst} className={className} />
}

工作結(jié)果

代碼實(shí)例

function App() {
  const text = [
    '## ewrwewr',
    '---',
    '俄方溫哥華娃陪我佛文件噢i人家范圍普及共軛分為惡狗和烹飪法人家哦我i俄加入微軟近日揮金如土口味看空間哦文件人品就感覺哦入耳幾個(gè)鵝各位趕緊哦額日記更可怕人間極品微積分i文件',
    '**14234**',
    '> 23123',
    '| 人玩兒玩兒完 | 人玩兒而為                | tyre已) |',
    '| ------------ | --------------------- | ---------------- |',
    '| 一天如一日         | 人特人特人圖          | 100              |',
    '| 3而特人       | 的黑寡婦恢復(fù)的很發(fā)達(dá)| 50               |',
    '| 而特特         | 好的風(fēng)格很煩很煩他 | 30               |',
  ].join('\n')
  return (
    <div>
      <Markdown text={text} />
    </div>
  );
}

實(shí)際效果

頁(yè)面截圖

DOM截圖
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末易结,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌搞动,老刑警劉巖躏精,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異鹦肿,居然都是意外死亡矗烛,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門箩溃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瞭吃,“玉大人,你說我怎么就攤上這事涣旨⊥峒埽” “怎么了?”我有些...
    開封第一講書人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵开泽,是天一觀的道長(zhǎng)牡拇。 經(jīng)常有香客問我,道長(zhǎng)穆律,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任导俘,我火速辦了婚禮峦耘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘旅薄。我一直安慰自己辅髓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開白布少梁。 她就那樣靜靜地躺著洛口,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凯沪。 梳的紋絲不亂的頭發(fā)上第焰,一...
    開封第一講書人閱讀 52,268評(píng)論 1 309
  • 那天,我揣著相機(jī)與錄音妨马,去河邊找鬼挺举。 笑死,一個(gè)胖子當(dāng)著我的面吹牛烘跺,可吹牛的內(nèi)容都是我干的湘纵。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼滤淳,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼梧喷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤铺敌,失蹤者是張志新(化名)和其女友劉穎汇歹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體适刀,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秤朗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了笔喉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片取视。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖常挚,靈堂內(nèi)的尸體忽然破棺而出作谭,到底是詐尸還是另有隱情,我是刑警寧澤奄毡,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布折欠,位于F島的核電站,受9級(jí)特大地震影響吼过,放射性物質(zhì)發(fā)生泄漏锐秦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一盗忱、第九天 我趴在偏房一處隱蔽的房頂上張望酱床。 院中可真熱鬧,春花似錦趟佃、人聲如沸扇谣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽罐寨。三九已至,卻和暖如春序矩,著一層夾襖步出監(jiān)牢的瞬間鸯绿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工贮泞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留楞慈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓啃擦,卻偏偏與公主長(zhǎng)得像囊蓝,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子令蛉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容