Feature/highlightjs (#242)

* Add highlighting for code blocks

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
mrdrogdrog 2020-06-22 22:39:14 +02:00 committed by GitHub
parent b3899cd1a5
commit e03da3bd76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 170 additions and 2 deletions

View file

@ -0,0 +1,33 @@
.markdown-body pre code {
&.hljs {
display: flex;
flex-direction: row;
}
.linenumbers {
text-align: right;
position: relative;
cursor: default;
z-index: 4;
padding: 0 8px 0 0;
min-width: 20px;
box-sizing: content-box;
color: #afafaf;
border-right: 3px solid #6ce26c;
display: flex;
flex-direction: column;
float: left;
overflow: hidden;
user-select: none;
& > span:before {
content: attr(data-line-number);
}
}
.code {
float: left;
margin: 0 0 0 16px;
}
}

View file

@ -0,0 +1,66 @@
import hljs from 'highlight.js'
import React, { useMemo } from 'react'
import ReactHtmlParser from 'react-html-parser'
import { ShowIf } from '../show-if/show-if'
import './highlighted-code.scss'
export interface HighlightedCodeProps {
code: string,
language?: string,
showGutter: boolean
}
export const escapeHtml = (unsafe: string): string => {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
const checkIfLanguageIsSupported = (language: string):boolean => {
return hljs.listLanguages().indexOf(language) > -1
}
const correctLanguage = (language: string|undefined): string|undefined => {
switch (language) {
case 'html':
return 'xml'
default:
return language
}
}
export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language, showGutter }) => {
const highlightedCode = useMemo(() => {
const replacedLanguage = correctLanguage(language)
return ((!!replacedLanguage && checkIfLanguageIsSupported(replacedLanguage)) ? hljs.highlight(replacedLanguage, code).value : escapeHtml(code))
.split('\n')
.filter(line => !!line)
.map(line => ReactHtmlParser(line))
}, [code, language])
return (
<code className={'hljs'}>
<ShowIf condition={showGutter}>
<span className={'linenumbers'}>
{
highlightedCode
.map((line, index) => {
return <span data-line-number={index + 1}/>
})
}
</span>
</ShowIf>
<span className={'code'}>
{
highlightedCode
.map((line, index) =>
<div key={index} className={'line'}>
{line}
</div>)
}
</span>
</code>)
}

View file

@ -28,7 +28,14 @@ https://www.youtube.com/watch?v=KgMpKsp23yY
https://vimeo.com/23237102
## PDF
{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}`)
{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}
## Code highlighting
\`\`\`javascript=
let a = 1
\`\`\`
`)
const isWide = useMedia({ minWidth: 576 })
const [firstDraw, setFirstDraw] = useState(true)

View file

@ -0,0 +1,23 @@
import MarkdownIt from 'markdown-it/lib'
const highlightRegex = /^(\w*)(=?)$/
export const highlightedCode: MarkdownIt.PluginSimple = (md: MarkdownIt) => {
md.core.ruler.push('highlighted-code', (state) => {
state.tokens.forEach(token => {
if (token.type === 'fence') {
const highlightInfos = highlightRegex.exec(token.info)
if (!highlightInfos) {
return
}
if (highlightInfos[1]) {
token.attrJoin('data-highlight-language', highlightInfos[1])
}
if (highlightInfos[2]) {
token.attrJoin('data-show-gutter', '')
}
}
})
return true
})
}

View file

@ -16,6 +16,7 @@ import taskList from 'markdown-it-task-lists'
import React, { ReactElement, useMemo } from 'react'
import ReactHtmlParser, { convertNodeToElement, Transform } from 'react-html-parser'
import { createRenderContainer, validAlertLevels } from './container-plugins/alert'
import { highlightedCode } from './markdown-it-plugins/highlighted-code'
import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
import './markdown-renderer.scss'
import { replaceGistLink } from './regex-plugins/replace-gist-link'
@ -28,6 +29,7 @@ import { replacePdfShortCode } from './regex-plugins/replace-pdf-short-code'
import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
import { getGistReplacement } from './replace-components/gist/gist-frame'
import { getHighlightedCodeBlock } from './replace-components/highlighted-code/highlighted-code'
import { getPDFReplacement } from './replace-components/pdf/pdf-frame'
import { getTOCReplacement } from './replace-components/toc/toc-replacer'
import { getVimeoReplacement } from './replace-components/vimeo/vimeo-frame'
@ -41,7 +43,7 @@ export type SubNodeConverter = (node: DomElement, index: number) => ReactElement
export type ComponentReplacer = (node: DomElement, index: number, counterMap: Map<string, number>, nodeConverter: SubNodeConverter) => (ReactElement | undefined);
type ComponentReplacer2Identifier2CounterMap = Map<ComponentReplacer, Map<string, number>>
const allComponentReplacers: ComponentReplacer[] = [getYouTubeReplacement, getVimeoReplacement, getGistReplacement, getPDFReplacement, getTOCReplacement]
const allComponentReplacers: ComponentReplacer[] = [getYouTubeReplacement, getVimeoReplacement, getGistReplacement, getPDFReplacement, getTOCReplacement, getHighlightedCodeBlock]
const tryToReplaceNode = (node: DomElement, index:number, componentReplacer2Identifier2CounterMap: ComponentReplacer2Identifier2CounterMap, nodeConverter: SubNodeConverter) => {
return allComponentReplacers
@ -87,6 +89,7 @@ const MarkdownRenderer: React.FC<MarkdownPreviewProps> = ({ content }) => {
md.use(markdownItRegex, replaceYouTubeLink)
md.use(markdownItRegex, replaceVimeoLink)
md.use(markdownItRegex, replaceGistLink)
md.use(highlightedCode)
md.use(MarkdownItParserDebugger)
validAlertLevels.forEach(level => {

View file

@ -0,0 +1,3 @@
.markdown-body {
@import '../../../../../../node_modules/highlight.js/styles/github-gist';
}

View file

@ -0,0 +1,16 @@
import { DomElement } from 'domhandler'
import React, { ReactElement } from 'react'
import { HighlightedCode } from '../../../../common/highlighted-code/highlighted-code'
import './highlighted-code.scss'
const getElementReplacement = (codeNode: DomElement, index: number, counterMap: Map<string, number>): (ReactElement | undefined) => {
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || !codeNode.children || !codeNode.children[0]) {
return
}
const language = codeNode.attribs['data-highlight-language']
const showGutter = codeNode.attribs['data-show-gutter'] !== undefined
return <HighlightedCode key={index} language={language} showGutter={showGutter} code={codeNode.children[0].data as string}/>
}
export { getElementReplacement as getHighlightedCodeBlock }