From 7c90b1eb12773e28d55fc8daab3d59596f64ff6a Mon Sep 17 00:00:00 2001 From: Futa Hirakoba Date: Mon, 29 Jun 2020 11:27:24 +0900 Subject: [PATCH] feat: Enabled syntax highlight to a code block --- package-lock.json | 5 ++ package.json | 1 + .../markdown/renderer/highlight-settings.ts | 53 ++++++++++++++++++ .../markdown/renderer/marktone-renderer.ts | 54 +++++++++++++++++-- 4 files changed, 108 insertions(+), 5 deletions(-) create mode 100644 src/app/markdown/renderer/highlight-settings.ts diff --git a/package-lock.json b/package-lock.json index 64879630..749c7294 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5343,6 +5343,11 @@ "minimalistic-assert": "^1.0.1" } }, + "highlight.js": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.1.1.tgz", + "integrity": "sha512-b4L09127uVa+9vkMgPpdUQP78ickGbHEQTWeBrQFTJZ4/n2aihWOGS0ZoUqAwjVmfjhq/C76HRzkqwZhK4sBbg==" + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", diff --git a/package.json b/package.json index 53625357..fb51cf88 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@types/webscopeio__react-textarea-autocomplete": "4.6.1", "@webscopeio/react-textarea-autocomplete": "4.6.3", "dompurify": "2.0.12", + "highlight.js": "10.1.1", "marked": "1.0.0", "react": "16.13.1", "react-dom": "16.13.1" diff --git a/src/app/markdown/renderer/highlight-settings.ts b/src/app/markdown/renderer/highlight-settings.ts new file mode 100644 index 00000000..aeaba91d --- /dev/null +++ b/src/app/markdown/renderer/highlight-settings.ts @@ -0,0 +1,53 @@ +const red = "color: #d91e18;"; +const orange = "color: #aa5d00;"; +const yellow = "color: #ffd700;"; +const green = "color: #008000;"; +const blue = "color: #007faa;"; +const purple = "color: #7928a1;"; +const bold = "font-weight: bold;"; + +export const highlightStyles: { [index: string]: string } = { + "hljs-comment": "color: #696969;", + "hljs-quote": "color: #696969;", + "hljs-meta": orange, + "hljs-meta-keyword": orange, + "hljs-meta-string": blue, + "hljs-variable": red, + "hljs-template-variable": red, + "hljs-tag": red, + "hljs-name": red, + "hljs-selector-id": red, + "hljs-selector-class": red, + "hljs-regexp": red, + "hljs-deletion": red, + "hljs-number": orange, + "hljs-built_in": orange, + "hljs-builtin-name": orange, + "hljs-literal": orange, + "hljs-type": orange, + "hljs-params": "", + "hljs-link": orange, + "hljs-attribute": yellow, + "hljs-string": green, + "hljs-symbol": green, + "hljs-bullet": green, + "hljs-addition": green, + "hljs-title": blue, + "hljs-section": blue, + "hljs-keyword": purple, + "hljs-selector-tag": purple, + "hljs-emphasis": "font-style: italic;", + "hljs-strong": bold, + "hljs-class": bold, +}; + +export const languageAliases: { [index: string]: string } = { + zsh: "bash", + sh: "bash", + "c++": "cpp", + html: "xml", + js: "javascript", + ts: "typescript", + kt: "kotlin", + yaml: "yml", +}; diff --git a/src/app/markdown/renderer/marktone-renderer.ts b/src/app/markdown/renderer/marktone-renderer.ts index be6cdc87..70274437 100644 --- a/src/app/markdown/renderer/marktone-renderer.ts +++ b/src/app/markdown/renderer/marktone-renderer.ts @@ -1,11 +1,13 @@ import { MarkedOptions, Renderer } from "marked"; import MentionReplacer from "../replacer/mention-replacer"; import KintoneClient from "../../kintone/kintone-client"; +import hljs from "highlight.js"; +import { highlightStyles, languageAliases } from "./highlight-settings"; class MarktoneRendererHelper { static escapeHTML(html: string): string { const escapeTest = /[&<>"']/; - const escapeReplace = /[&<>"']/g; + const escapeReplace = new RegExp(escapeTest, "g"); const replacements: { [key: string]: string } = { "&": "&", "<": "<", @@ -20,6 +22,43 @@ class MarktoneRendererHelper { return html; } + + static unescapeHTML(html: string): string { + const unescapeTest = /(&(?:lt|amp|gt|quot|#39);)/; + const unescapeReplace = new RegExp(unescapeTest, "g"); + const replacements: { [key: string]: string } = { + "&": "&", + "<": "<", + ">": ">", + """: '"', + "'": "'", + }; + + if (unescapeTest.test(html)) { + return html.replace(unescapeReplace, (ch) => replacements[ch]); + } + + return html; + } + + static highlightCode(code: string, specifiedLanguage: string): string { + let language = specifiedLanguage; + + if (!hljs.listLanguages().includes(specifiedLanguage)) { + language = languageAliases[specifiedLanguage] || "plaintext"; + } + const highlightedCode = hljs.highlight(language, code).value; + const highlightedCodeWithInlineStyle = highlightedCode.replace( + /class="([\w-]+)"/g, + (matchedString, className) => { + const style = highlightStyles[className]; + if (style === undefined) return matchedString; + return `style="${style}"`; + } + ); + + return highlightedCodeWithInlineStyle; + } } interface Render { @@ -67,15 +106,20 @@ class MarktoneRenderer extends Renderer { } code(code: string, language: string, isEscaped: boolean): string { - const escapedCode = isEscaped - ? code - : MarktoneRendererHelper.escapeHTML(code); + const unescapedCode = isEscaped + ? MarktoneRendererHelper.unescapeHTML(code) + : code; + const escapedCodeWithHighlight = MarktoneRendererHelper.highlightCode( + unescapedCode, + language + ); + const preStyle = "background-color: #f6f8fa; border-radius: 3px; padding: 8px 16px;"; const codeStyle = `font-family: ${this.monospaceFontFamiliesString};`; console.log(codeStyle); - return `
${escapedCode}
`; + return `
${escapedCodeWithHighlight}
`; } blockquote(quote: string): string {