From e03da3bd769646b94ec53a362eb14a3e6201577b Mon Sep 17 00:00:00 2001 From: mrdrogdrog Date: Mon, 22 Jun 2020 22:39:14 +0200 Subject: [PATCH] Feature/highlightjs (#242) * Add highlighting for code blocks Signed-off-by: Tilman Vatteroth --- CHANGELOG.md | 5 ++ package.json | 2 + .../highlighted-code/highlighted-code.scss | 33 ++++++++++ .../highlighted-code/highlighted-code.tsx | 66 +++++++++++++++++++ src/components/editor/editor.tsx | 9 ++- .../markdown-it-plugins/highlighted-code.ts | 23 +++++++ .../markdown-renderer/markdown-renderer.tsx | 5 +- .../highlighted-code/highlighted-code.scss | 3 + .../highlighted-code/highlighted-code.tsx | 16 +++++ yarn.lock | 10 +++ 10 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 src/components/common/highlighted-code/highlighted-code.scss create mode 100644 src/components/common/highlighted-code/highlighted-code.tsx create mode 100644 src/components/editor/markdown-renderer/markdown-it-plugins/highlighted-code.ts create mode 100644 src/components/editor/markdown-renderer/replace-components/highlighted-code/highlighted-code.scss create mode 100644 src/components/editor/markdown-renderer/replace-components/highlighted-code/highlighted-code.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 47af3308c..3cb618584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ - If a legacy embedding code is detected it will show the link to the presentation instead of the embedded presentation - Speakerdeck embedding - 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 diff --git a/package.json b/package.json index 1dc496caa..a5ff86c01 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@testing-library/react": "10.3.0", "@testing-library/user-event": "12.0.2", "@types/codemirror": "0.0.96", + "@types/highlight.js": "^9.12.4", "@types/jest": "26.0.0", "@types/markdown-it": "10.0.1", "@types/markdown-it-anchor": "4.0.4", @@ -36,6 +37,7 @@ "eslint-plugin-standard": "4.0.1", "fork-awesome": "1.1.7", "github-markdown-css": "4.0.0", + "highlight.js": "^10.1.1", "i18next": "19.4.5", "i18next-browser-languagedetector": "5.0.0", "i18next-http-backend": "1.0.15", diff --git a/src/components/common/highlighted-code/highlighted-code.scss b/src/components/common/highlighted-code/highlighted-code.scss new file mode 100644 index 000000000..1ae131ac9 --- /dev/null +++ b/src/components/common/highlighted-code/highlighted-code.scss @@ -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; + } +} diff --git a/src/components/common/highlighted-code/highlighted-code.tsx b/src/components/common/highlighted-code/highlighted-code.tsx new file mode 100644 index 000000000..bf537809f --- /dev/null +++ b/src/components/common/highlighted-code/highlighted-code.tsx @@ -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, ''') +} + +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 = ({ 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 ( + + + + { + highlightedCode + .map((line, index) => { + return + }) + } + + + + { + highlightedCode + .map((line, index) => +
+ {line} +
) + } +
+
) +} diff --git a/src/components/editor/editor.tsx b/src/components/editor/editor.tsx index a2549e1b2..6050341d3 100644 --- a/src/components/editor/editor.tsx +++ b/src/components/editor/editor.tsx @@ -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) diff --git a/src/components/editor/markdown-renderer/markdown-it-plugins/highlighted-code.ts b/src/components/editor/markdown-renderer/markdown-it-plugins/highlighted-code.ts new file mode 100644 index 000000000..b2ea8e57e --- /dev/null +++ b/src/components/editor/markdown-renderer/markdown-it-plugins/highlighted-code.ts @@ -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 + }) +} diff --git a/src/components/editor/markdown-renderer/markdown-renderer.tsx b/src/components/editor/markdown-renderer/markdown-renderer.tsx index c2f64d5f7..7b53ca150 100644 --- a/src/components/editor/markdown-renderer/markdown-renderer.tsx +++ b/src/components/editor/markdown-renderer/markdown-renderer.tsx @@ -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, nodeConverter: SubNodeConverter) => (ReactElement | undefined); type ComponentReplacer2Identifier2CounterMap = Map> -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 = ({ content }) => { md.use(markdownItRegex, replaceYouTubeLink) md.use(markdownItRegex, replaceVimeoLink) md.use(markdownItRegex, replaceGistLink) + md.use(highlightedCode) md.use(MarkdownItParserDebugger) validAlertLevels.forEach(level => { diff --git a/src/components/editor/markdown-renderer/replace-components/highlighted-code/highlighted-code.scss b/src/components/editor/markdown-renderer/replace-components/highlighted-code/highlighted-code.scss new file mode 100644 index 000000000..c954bdca5 --- /dev/null +++ b/src/components/editor/markdown-renderer/replace-components/highlighted-code/highlighted-code.scss @@ -0,0 +1,3 @@ +.markdown-body { + @import '../../../../../../node_modules/highlight.js/styles/github-gist'; +} diff --git a/src/components/editor/markdown-renderer/replace-components/highlighted-code/highlighted-code.tsx b/src/components/editor/markdown-renderer/replace-components/highlighted-code/highlighted-code.tsx new file mode 100644 index 000000000..521006759 --- /dev/null +++ b/src/components/editor/markdown-renderer/replace-components/highlighted-code/highlighted-code.tsx @@ -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): (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 +} + +export { getElementReplacement as getHighlightedCodeBlock } diff --git a/yarn.lock b/yarn.lock index a4a2f0413..225e1e394 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1514,6 +1514,11 @@ "@types/minimatch" "*" "@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@*": version "4.7.5" 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" 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: version "4.10.1" resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"