準(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截圖