mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-26 03:33:58 -05:00
Feature/highlightjs (#242)
* Add highlighting for code blocks Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
b3899cd1a5
commit
e03da3bd76
10 changed files with 170 additions and 2 deletions
|
@ -16,6 +16,11 @@
|
||||||
- If a legacy embedding code is detected it will show the link to the presentation instead of the embedded presentation
|
- If a legacy embedding code is detected it will show the link to the presentation instead of the embedded presentation
|
||||||
- Speakerdeck embedding
|
- Speakerdeck embedding
|
||||||
- If a legacy embedding code is detected it will show the link to the presentation instead of the embedded presentation
|
- If a legacy embedding code is detected it will show the link to the presentation instead of the embedded presentation
|
||||||
|
- We are now using `highlight.js` instead of `highlight.js` + `prism.js` for code highlighting. Check out the [highlight.js demo page](https://highlightjs.org/static/demo/) to see which languages are supported.
|
||||||
|
The highlighting for following languages isn't supported by `highlight.js`:
|
||||||
|
- tiddlywiki
|
||||||
|
- mediawiki
|
||||||
|
- jsx
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"@testing-library/react": "10.3.0",
|
"@testing-library/react": "10.3.0",
|
||||||
"@testing-library/user-event": "12.0.2",
|
"@testing-library/user-event": "12.0.2",
|
||||||
"@types/codemirror": "0.0.96",
|
"@types/codemirror": "0.0.96",
|
||||||
|
"@types/highlight.js": "^9.12.4",
|
||||||
"@types/jest": "26.0.0",
|
"@types/jest": "26.0.0",
|
||||||
"@types/markdown-it": "10.0.1",
|
"@types/markdown-it": "10.0.1",
|
||||||
"@types/markdown-it-anchor": "4.0.4",
|
"@types/markdown-it-anchor": "4.0.4",
|
||||||
|
@ -36,6 +37,7 @@
|
||||||
"eslint-plugin-standard": "4.0.1",
|
"eslint-plugin-standard": "4.0.1",
|
||||||
"fork-awesome": "1.1.7",
|
"fork-awesome": "1.1.7",
|
||||||
"github-markdown-css": "4.0.0",
|
"github-markdown-css": "4.0.0",
|
||||||
|
"highlight.js": "^10.1.1",
|
||||||
"i18next": "19.4.5",
|
"i18next": "19.4.5",
|
||||||
"i18next-browser-languagedetector": "5.0.0",
|
"i18next-browser-languagedetector": "5.0.0",
|
||||||
"i18next-http-backend": "1.0.15",
|
"i18next-http-backend": "1.0.15",
|
||||||
|
|
33
src/components/common/highlighted-code/highlighted-code.scss
Normal file
33
src/components/common/highlighted-code/highlighted-code.scss
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
66
src/components/common/highlighted-code/highlighted-code.tsx
Normal file
66
src/components/common/highlighted-code/highlighted-code.tsx
Normal 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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
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>)
|
||||||
|
}
|
|
@ -28,7 +28,14 @@ https://www.youtube.com/watch?v=KgMpKsp23yY
|
||||||
https://vimeo.com/23237102
|
https://vimeo.com/23237102
|
||||||
|
|
||||||
## PDF
|
## 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 isWide = useMedia({ minWidth: 576 })
|
||||||
const [firstDraw, setFirstDraw] = useState(true)
|
const [firstDraw, setFirstDraw] = useState(true)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import taskList from 'markdown-it-task-lists'
|
||||||
import React, { ReactElement, useMemo } from 'react'
|
import React, { ReactElement, useMemo } from 'react'
|
||||||
import ReactHtmlParser, { convertNodeToElement, Transform } from 'react-html-parser'
|
import ReactHtmlParser, { convertNodeToElement, Transform } from 'react-html-parser'
|
||||||
import { createRenderContainer, validAlertLevels } from './container-plugins/alert'
|
import { createRenderContainer, validAlertLevels } from './container-plugins/alert'
|
||||||
|
import { highlightedCode } from './markdown-it-plugins/highlighted-code'
|
||||||
import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
|
import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
|
||||||
import './markdown-renderer.scss'
|
import './markdown-renderer.scss'
|
||||||
import { replaceGistLink } from './regex-plugins/replace-gist-link'
|
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 { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
|
||||||
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
|
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
|
||||||
import { getGistReplacement } from './replace-components/gist/gist-frame'
|
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 { getPDFReplacement } from './replace-components/pdf/pdf-frame'
|
||||||
import { getTOCReplacement } from './replace-components/toc/toc-replacer'
|
import { getTOCReplacement } from './replace-components/toc/toc-replacer'
|
||||||
import { getVimeoReplacement } from './replace-components/vimeo/vimeo-frame'
|
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);
|
export type ComponentReplacer = (node: DomElement, index: number, counterMap: Map<string, number>, nodeConverter: SubNodeConverter) => (ReactElement | undefined);
|
||||||
type ComponentReplacer2Identifier2CounterMap = Map<ComponentReplacer, Map<string, number>>
|
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) => {
|
const tryToReplaceNode = (node: DomElement, index:number, componentReplacer2Identifier2CounterMap: ComponentReplacer2Identifier2CounterMap, nodeConverter: SubNodeConverter) => {
|
||||||
return allComponentReplacers
|
return allComponentReplacers
|
||||||
|
@ -87,6 +89,7 @@ const MarkdownRenderer: React.FC<MarkdownPreviewProps> = ({ content }) => {
|
||||||
md.use(markdownItRegex, replaceYouTubeLink)
|
md.use(markdownItRegex, replaceYouTubeLink)
|
||||||
md.use(markdownItRegex, replaceVimeoLink)
|
md.use(markdownItRegex, replaceVimeoLink)
|
||||||
md.use(markdownItRegex, replaceGistLink)
|
md.use(markdownItRegex, replaceGistLink)
|
||||||
|
md.use(highlightedCode)
|
||||||
md.use(MarkdownItParserDebugger)
|
md.use(MarkdownItParserDebugger)
|
||||||
|
|
||||||
validAlertLevels.forEach(level => {
|
validAlertLevels.forEach(level => {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
.markdown-body {
|
||||||
|
@import '../../../../../../node_modules/highlight.js/styles/github-gist';
|
||||||
|
}
|
|
@ -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 }
|
10
yarn.lock
10
yarn.lock
|
@ -1514,6 +1514,11 @@
|
||||||
"@types/minimatch" "*"
|
"@types/minimatch" "*"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/highlight.js@^9.12.4":
|
||||||
|
version "9.12.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.4.tgz#8c3496bd1b50cc04aeefd691140aa571d4dbfa34"
|
||||||
|
integrity sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==
|
||||||
|
|
||||||
"@types/history@*":
|
"@types/history@*":
|
||||||
version "4.7.5"
|
version "4.7.5"
|
||||||
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860"
|
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860"
|
||||||
|
@ -5502,6 +5507,11 @@ hex-color-regex@^1.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
||||||
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
||||||
|
|
||||||
|
highlight.js@^10.1.1:
|
||||||
|
version "10.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.1.1.tgz#691a2148a8d922bf12e52a294566a0d993b94c57"
|
||||||
|
integrity sha512-b4L09127uVa+9vkMgPpdUQP78ickGbHEQTWeBrQFTJZ4/n2aihWOGS0ZoUqAwjVmfjhq/C76HRzkqwZhK4sBbg==
|
||||||
|
|
||||||
history@^4.9.0:
|
history@^4.9.0:
|
||||||
version "4.10.1"
|
version "4.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
||||||
|
|
Loading…
Reference in a new issue