Skip to content

Commit

Permalink
Introduce MarkdownPreviewer
Browse files Browse the repository at this point in the history
  • Loading branch information
Rokt33r committed Sep 23, 2019
1 parent bba8d68 commit db2e16c
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 12 deletions.
163 changes: 163 additions & 0 deletions src/components/atoms/MarkdownPreviewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import React, { useEffect } from 'react'
import unified from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeRaw from 'rehype-raw'
import rehypeSanitize from 'rehype-sanitize'
import rehypeReact from 'rehype-react'
import gh from 'hast-util-sanitize/lib/github.json'
import { mergeDeepRight } from 'ramda'
import visit from 'unist-util-visit'
import { Node, Parent } from 'unist'
import CodeMirror from '../../lib/CodeMirror'
import toText from 'hast-util-to-text'
import h from 'hastscript'
import useForceUpdate from 'use-force-update'

const schema = mergeDeepRight(gh, { attributes: { '*': ['className'] } })

interface Element extends Node {
type: 'element'
properties: { [key: string]: any }
}

function getMime(name: string) {
const modeInfo = CodeMirror.findModeByName(name)
if (modeInfo == null) return null
return modeInfo.mime || modeInfo.mimes![0]
}

const markdownProcessor = unified()
.use(remarkParse)
.use(remarkRehype, { allowDangerousHTML: false })
.use((options: any) => {
const settings = options || {}
const detect = settings.subset !== false
const ignoreMissing = settings.ignoreMissing
const plainText = settings.plainText || []
return function(tree: Parent) {
visit<Element>(tree, 'element', visitor)

function visitor(node: Element, _index: number, parent: Node) {
const props = node.properties
let result

if (!parent || parent.tagName !== 'pre' || node.tagName !== 'code') {
return
}

const lang = language(node)

if (
lang === false ||
(!lang && !detect) ||
plainText.indexOf(lang) !== -1
) {
return
}

if (!props.className) {
props.className = []
}

if (props.className.indexOf(name) === -1) {
props.className.unshift(name)
}

try {
const text = toText(parent)
const cmResult = [] as Node[]
if (lang != null) {
const mime = getMime(lang)
if (mime != null) {
CodeMirror.runMode(text, mime, (text, style) => {
cmResult.push(
h(
'span',
{
className: style
? 'cm-' + style.replace(/ +/g, ' cm-')
: undefined
},
text
)
)
})
}
}
result = {
language: lang,
value: cmResult
}
} catch (error) {
if (
error &&
ignoreMissing &&
/Unknown language/.test(error.message)
) {
return
}

throw error
}

props.className.push('cm-s-default')
if (!lang && result.language) {
props.className.push('language-' + result.language)
}

node.children = result.value
}

// Get the programming language of `node`.
function language(node: Element) {
const className = node.properties.className || []
const length = className.length
let index = -1
let value

while (++index < length) {
value = className[index]

if (value === 'no-highlight' || value === 'nohighlight') {
return false
}

if (value.slice(0, 5) === 'lang-') {
return value.slice(5)
}

if (value.slice(0, 9) === 'language-') {
return value.slice(9)
}
}

return null
}
}
})
.use(rehypeRaw)
.use(rehypeSanitize, schema)
.use(rehypeReact, { createElement: React.createElement })

interface MarkdownPreviewerProps {
content: string
}

const MarkdownPreviewer = ({ content }: MarkdownPreviewerProps) => {
const forceUpdate = useForceUpdate()

useEffect(
() => {
window.addEventListener('codemirror-mode-load', forceUpdate)
return () => {
window.removeEventListener('codemirror-mode-load', forceUpdate)
}
},
[forceUpdate]
)

return <div>{markdownProcessor.processSync(content).contents}</div>
}

export default MarkdownPreviewer
26 changes: 20 additions & 6 deletions src/lib/CodeMirror.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import CodeMirror from 'codemirror'
import 'codemirror/addon/runmode/runmode'
import 'codemirror/addon/mode/overlay'
import 'codemirror/mode/markdown/markdown'
import debounce from 'lodash/debounce'

window.CodeMirror = CodeMirror

Expand All @@ -22,6 +24,23 @@ declare module 'codemirror' {
}

function findModeByMIME(mime: string): ModeInfo | undefined
function findModeByName(name: string): ModeInfo | undefined

function runMode(
text: string,
modespec: any,
callback: HTMLElement | ((text: string, style: string | null) => void),
options?: { tabSize?: number; state?: any }
): void
}

const dispatchModeLoad = debounce(() => {
window.dispatchEvent(new CustomEvent('codemirror-mode-load'))
}, 300)

export async function requireMode(mode: string) {
await import(`codemirror/mode/${mode}/${mode}.js`)
dispatchModeLoad()
}

function loadMode(_CodeMirror: any) {
Expand All @@ -35,15 +54,10 @@ function loadMode(_CodeMirror: any) {
return result
}

async function requireMode(mode: string) {
await import(`codemirror/mode/${mode}/${mode}.js`)
window.dispatchEvent(new CustomEvent('codemirror-mode-load'))
}

const originalGetMode = CodeMirror.getMode
_CodeMirror.getMode = (config: CodeMirror.EditorConfiguration, mime: any) => {
const modeObj = originalGetMode(config, mime)
if (modeObj.name === 'null') {
if (modeObj.name === 'null' && typeof mime === 'string') {
const mode = findModeByMIME(mime)
if (mode != null) {
requireMode(mode.mode)
Expand Down
13 changes: 7 additions & 6 deletions typings/unified.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
declare module 'unified'
declare module 'unified-*'
declare module 'unist-util-*'
declare module 'remark'
declare module 'remark-*'
declare module 'mdast-util-*'
declare module 'remark-rehype'
declare module 'rehype-raw'
declare module 'rehype-sanitize'
declare module 'rehype-react'
declare module 'hast-util-sanitize/lib/github.json'
declare module 'hast-util-to-text'
declare module 'hastscript'

0 comments on commit db2e16c

Please sign in to comment.