Skip to content

Commit db2e16c

Browse files
committed
Introduce MarkdownPreviewer
1 parent bba8d68 commit db2e16c

File tree

3 files changed

+190
-12
lines changed

3 files changed

+190
-12
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import React, { useEffect } from 'react'
2+
import unified from 'unified'
3+
import remarkParse from 'remark-parse'
4+
import remarkRehype from 'remark-rehype'
5+
import rehypeRaw from 'rehype-raw'
6+
import rehypeSanitize from 'rehype-sanitize'
7+
import rehypeReact from 'rehype-react'
8+
import gh from 'hast-util-sanitize/lib/github.json'
9+
import { mergeDeepRight } from 'ramda'
10+
import visit from 'unist-util-visit'
11+
import { Node, Parent } from 'unist'
12+
import CodeMirror from '../../lib/CodeMirror'
13+
import toText from 'hast-util-to-text'
14+
import h from 'hastscript'
15+
import useForceUpdate from 'use-force-update'
16+
17+
const schema = mergeDeepRight(gh, { attributes: { '*': ['className'] } })
18+
19+
interface Element extends Node {
20+
type: 'element'
21+
properties: { [key: string]: any }
22+
}
23+
24+
function getMime(name: string) {
25+
const modeInfo = CodeMirror.findModeByName(name)
26+
if (modeInfo == null) return null
27+
return modeInfo.mime || modeInfo.mimes![0]
28+
}
29+
30+
const markdownProcessor = unified()
31+
.use(remarkParse)
32+
.use(remarkRehype, { allowDangerousHTML: false })
33+
.use((options: any) => {
34+
const settings = options || {}
35+
const detect = settings.subset !== false
36+
const ignoreMissing = settings.ignoreMissing
37+
const plainText = settings.plainText || []
38+
return function(tree: Parent) {
39+
visit<Element>(tree, 'element', visitor)
40+
41+
function visitor(node: Element, _index: number, parent: Node) {
42+
const props = node.properties
43+
let result
44+
45+
if (!parent || parent.tagName !== 'pre' || node.tagName !== 'code') {
46+
return
47+
}
48+
49+
const lang = language(node)
50+
51+
if (
52+
lang === false ||
53+
(!lang && !detect) ||
54+
plainText.indexOf(lang) !== -1
55+
) {
56+
return
57+
}
58+
59+
if (!props.className) {
60+
props.className = []
61+
}
62+
63+
if (props.className.indexOf(name) === -1) {
64+
props.className.unshift(name)
65+
}
66+
67+
try {
68+
const text = toText(parent)
69+
const cmResult = [] as Node[]
70+
if (lang != null) {
71+
const mime = getMime(lang)
72+
if (mime != null) {
73+
CodeMirror.runMode(text, mime, (text, style) => {
74+
cmResult.push(
75+
h(
76+
'span',
77+
{
78+
className: style
79+
? 'cm-' + style.replace(/ +/g, ' cm-')
80+
: undefined
81+
},
82+
text
83+
)
84+
)
85+
})
86+
}
87+
}
88+
result = {
89+
language: lang,
90+
value: cmResult
91+
}
92+
} catch (error) {
93+
if (
94+
error &&
95+
ignoreMissing &&
96+
/Unknown language/.test(error.message)
97+
) {
98+
return
99+
}
100+
101+
throw error
102+
}
103+
104+
props.className.push('cm-s-default')
105+
if (!lang && result.language) {
106+
props.className.push('language-' + result.language)
107+
}
108+
109+
node.children = result.value
110+
}
111+
112+
// Get the programming language of `node`.
113+
function language(node: Element) {
114+
const className = node.properties.className || []
115+
const length = className.length
116+
let index = -1
117+
let value
118+
119+
while (++index < length) {
120+
value = className[index]
121+
122+
if (value === 'no-highlight' || value === 'nohighlight') {
123+
return false
124+
}
125+
126+
if (value.slice(0, 5) === 'lang-') {
127+
return value.slice(5)
128+
}
129+
130+
if (value.slice(0, 9) === 'language-') {
131+
return value.slice(9)
132+
}
133+
}
134+
135+
return null
136+
}
137+
}
138+
})
139+
.use(rehypeRaw)
140+
.use(rehypeSanitize, schema)
141+
.use(rehypeReact, { createElement: React.createElement })
142+
143+
interface MarkdownPreviewerProps {
144+
content: string
145+
}
146+
147+
const MarkdownPreviewer = ({ content }: MarkdownPreviewerProps) => {
148+
const forceUpdate = useForceUpdate()
149+
150+
useEffect(
151+
() => {
152+
window.addEventListener('codemirror-mode-load', forceUpdate)
153+
return () => {
154+
window.removeEventListener('codemirror-mode-load', forceUpdate)
155+
}
156+
},
157+
[forceUpdate]
158+
)
159+
160+
return <div>{markdownProcessor.processSync(content).contents}</div>
161+
}
162+
163+
export default MarkdownPreviewer

src/lib/CodeMirror.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import CodeMirror from 'codemirror'
2+
import 'codemirror/addon/runmode/runmode'
23
import 'codemirror/addon/mode/overlay'
34
import 'codemirror/mode/markdown/markdown'
5+
import debounce from 'lodash/debounce'
46

57
window.CodeMirror = CodeMirror
68

@@ -22,6 +24,23 @@ declare module 'codemirror' {
2224
}
2325

2426
function findModeByMIME(mime: string): ModeInfo | undefined
27+
function findModeByName(name: string): ModeInfo | undefined
28+
29+
function runMode(
30+
text: string,
31+
modespec: any,
32+
callback: HTMLElement | ((text: string, style: string | null) => void),
33+
options?: { tabSize?: number; state?: any }
34+
): void
35+
}
36+
37+
const dispatchModeLoad = debounce(() => {
38+
window.dispatchEvent(new CustomEvent('codemirror-mode-load'))
39+
}, 300)
40+
41+
export async function requireMode(mode: string) {
42+
await import(`codemirror/mode/${mode}/${mode}.js`)
43+
dispatchModeLoad()
2544
}
2645

2746
function loadMode(_CodeMirror: any) {
@@ -35,15 +54,10 @@ function loadMode(_CodeMirror: any) {
3554
return result
3655
}
3756

38-
async function requireMode(mode: string) {
39-
await import(`codemirror/mode/${mode}/${mode}.js`)
40-
window.dispatchEvent(new CustomEvent('codemirror-mode-load'))
41-
}
42-
4357
const originalGetMode = CodeMirror.getMode
4458
_CodeMirror.getMode = (config: CodeMirror.EditorConfiguration, mime: any) => {
4559
const modeObj = originalGetMode(config, mime)
46-
if (modeObj.name === 'null') {
60+
if (modeObj.name === 'null' && typeof mime === 'string') {
4761
const mode = findModeByMIME(mime)
4862
if (mode != null) {
4963
requireMode(mode.mode)

typings/unified.d.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
declare module 'unified'
2-
declare module 'unified-*'
3-
declare module 'unist-util-*'
4-
declare module 'remark'
5-
declare module 'remark-*'
6-
declare module 'mdast-util-*'
1+
declare module 'remark-rehype'
2+
declare module 'rehype-raw'
3+
declare module 'rehype-sanitize'
4+
declare module 'rehype-react'
5+
declare module 'hast-util-sanitize/lib/github.json'
6+
declare module 'hast-util-to-text'
7+
declare module 'hastscript'

0 commit comments

Comments
 (0)