From 4b191c7f68a13e1a9acb00b8b8c1b7638465f5f5 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 27 Jan 2019 14:34:52 +0900 Subject: [PATCH] [Client] Improve syntax highlighting Resolve #3926 Resolve #3390 --- CHANGELOG.md | 1 + package.json | 4 +- .../app/common/views/components/code-core.vue | 30 ++ .../app/common/views/components/code.vue | 28 ++ src/client/app/common/views/components/mfm.ts | 27 +- .../components/misskey-flavored-markdown.vue | 15 - src/mfm/syntax-highlight.ts | 343 ------------------ 7 files changed, 76 insertions(+), 372 deletions(-) create mode 100644 src/client/app/common/views/components/code-core.vue create mode 100644 src/client/app/common/views/components/code.vue delete mode 100644 src/mfm/syntax-highlight.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6325663fcd..d9e69e8f0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ unreleased * 外部サービス認証情報の配信 * 管理画面のモデレーションのUIを強化 * 管理画面からリモートユーザーの情報を更新できるように +* シンタックスハイライトの強化 * 引用投稿を削除したとき単なるRenoteとしてタイムラインに残る問題を修正 * イタリック構文の判定の改善 * タイトル構文の判定の改善 diff --git a/package.json b/package.json index d5afc934b3..b561b4de4c 100644 --- a/package.json +++ b/package.json @@ -97,8 +97,8 @@ "bootstrap-vue": "2.0.0-rc.11", "cafy": "12.0.0", "chai": "4.2.0", - "chalk": "2.4.2", "chai-http": "4.2.1", + "chalk": "2.4.2", "commander": "2.19.0", "crc-32": "1.2.0", "css-loader": "1.0.1", @@ -178,6 +178,7 @@ "parsimmon": "1.12.0", "portscanner": "2.2.0", "postcss-loader": "3.0.0", + "prismjs": "1.15.0", "progress-bar-webpack-plugin": "1.12.0", "promise-any": "0.2.0", "promise-limit": "2.7.0", @@ -230,6 +231,7 @@ "vue-js-modal": "1.3.28", "vue-loader": "15.5.1", "vue-marquee-text-component": "1.1.1", + "vue-prism-component": "1.1.1", "vue-router": "3.0.2", "vue-sequential-entrance": "1.1.3", "vue-style-loader": "4.1.2", diff --git a/src/client/app/common/views/components/code-core.vue b/src/client/app/common/views/components/code-core.vue new file mode 100644 index 0000000000..a50d943948 --- /dev/null +++ b/src/client/app/common/views/components/code-core.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/client/app/common/views/components/code.vue b/src/client/app/common/views/components/code.vue new file mode 100644 index 0000000000..d52c9f7bc2 --- /dev/null +++ b/src/client/app/common/views/components/code.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/client/app/common/views/components/mfm.ts b/src/client/app/common/views/components/mfm.ts index ad3d8204cc..8ffa566666 100644 --- a/src/client/app/common/views/components/mfm.ts +++ b/src/client/app/common/views/components/mfm.ts @@ -6,8 +6,8 @@ import MkUrl from './url.vue'; import MkMention from './mention.vue'; import { concat, sum } from '../../../../../prelude/array'; import MkFormula from './formula.vue'; +import MkCode from './code.vue'; import MkGoogle from './google.vue'; -import syntaxHighlight from '../../../../../mfm/syntax-highlight'; import { host } from '../../../config'; import { preorderF, countNodesF } from '../../../../../prelude/tree'; @@ -170,21 +170,22 @@ export default Vue.component('misskey-flavored-markdown', { } case 'blockCode': { - return [createElement('pre', { - class: 'code' - }, [ - createElement('code', { - domProps: { - innerHTML: syntaxHighlight(token.node.props.code) - } - }) - ])]; + return [createElement(MkCode, { + key: Math.random(), + props: { + code: token.node.props.code, + lang: token.node.props.lang, + } + })]; } case 'inlineCode': { - return [createElement('code', { - domProps: { - innerHTML: syntaxHighlight(token.node.props.code) + return [createElement(MkCode, { + key: Math.random(), + props: { + code: token.node.props.code, + lang: token.node.props.lang, + inline: true } })]; } diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.vue b/src/client/app/common/views/components/misskey-flavored-markdown.vue index 448df9aa88..5cb102240e 100644 --- a/src/client/app/common/views/components/misskey-flavored-markdown.vue +++ b/src/client/app/common/views/components/misskey-flavored-markdown.vue @@ -24,25 +24,10 @@ export default Vue.extend({ background var(--mfmTitleBg) border-radius 4px - >>> .code - margin 8px 0 - >>> .quote margin 8px padding 6px 0 6px 12px color var(--mfmQuote) border-left solid 3px var(--mfmQuoteLine) - >>> code - padding 4px 8px - margin 0 0.5em - font-size 90% - color #525252 - background var(--bg) - border-radius 2px - - >>> pre > code - padding 16px - margin 0 - diff --git a/src/mfm/syntax-highlight.ts b/src/mfm/syntax-highlight.ts deleted file mode 100644 index 109923fb71..0000000000 --- a/src/mfm/syntax-highlight.ts +++ /dev/null @@ -1,343 +0,0 @@ -import { capitalize, toUpperCase } from '../prelude/string'; - -function escape(text: string) { - return text - .replace(/>/g, '>') - .replace(/ b.length - a.length); - -const symbols = [ - '=', - '+', - '-', - '*', - '/', - '%', - '~', - '^', - '&', - '|', - '>', - '<', - '!', - '?' -]; - -type Token = { - html: string - next: number -}; - -type Element = (code: string, i: number, source: string) => (Token | null); - -const elements: Element[] = [ - // comment - code => { - if (code.substr(0, 2) != '//') return null; - const match = code.match(/^\/\/(.+?)(\n|$)/); - if (!match) return null; - const comment = match[0]; - return { - html: `${escape(comment)}`, - next: comment.length - }; - }, - - // block comment - code => { - const match = code.match(/^\/\*([\s\S]+?)\*\//); - if (!match) return null; - return { - html: `${escape(match[0])}`, - next: match[0].length - }; - }, - - // string - code => { - if (!/^['"`]/.test(code)) return null; - const begin = code[0]; - let str = begin; - let thisIsNotAString = false; - for (let i = 1; i < code.length; i++) { - const char = code[i]; - if (char == '\\') { - str += char; - str += code[i + 1] || ''; - i++; - continue; - } else if (char == begin) { - str += char; - break; - } else if (char == '\n' || i == (code.length - 1)) { - thisIsNotAString = true; - break; - } else { - str += char; - } - } - if (thisIsNotAString) { - return null; - } else { - return { - html: `${escape(str)}`, - next: str.length - }; - } - }, - - // regexp - code => { - if (code[0] != '/') return null; - let regexp = ''; - let thisIsNotARegexp = false; - for (let i = 1; i < code.length; i++) { - const char = code[i]; - if (char == '\\') { - regexp += char; - regexp += code[i + 1] || ''; - i++; - continue; - } else if (char == '/') { - break; - } else if (char == '\n' || i == (code.length - 1)) { - thisIsNotARegexp = true; - break; - } else { - regexp += char; - } - } - - if (thisIsNotARegexp) return null; - if (regexp == '') return null; - if (regexp.startsWith(' ') && regexp.endsWith(' ')) return null; - - return { - html: `/${escape(regexp)}/`, - next: regexp.length + 2 - }; - }, - - // label - code => { - if (code[0] != '@') return null; - const match = code.match(/^@([a-zA-Z_-]+?)\n/); - if (!match) return null; - const label = match[0]; - return { - html: `${label}`, - next: label.length - }; - }, - - // number - (code, i, source) => { - const prev = source[i - 1]; - if (prev && /[a-zA-Z]/.test(prev)) return null; - if (!/^[\-\+]?[0-9\.]+/.test(code)) return null; - const match = code.match(/^[\-\+]?[0-9\.]+/)[0]; - if (match) { - return { - html: `${match}`, - next: match.length - }; - } else { - return null; - } - }, - - // nan - (code, i, source) => { - const prev = source[i - 1]; - if (prev && /[a-zA-Z]/.test(prev)) return null; - if (code.substr(0, 3) == 'NaN') { - return { - html: `NaN`, - next: 3 - }; - } else { - return null; - } - }, - - // method - code => { - const match = code.match(/^([a-zA-Z_-]+?)\(/); - if (!match) return null; - - if (match[1] == '-') return null; - - return { - html: `${match[1]}`, - next: match[1].length - }; - }, - - // property - (code, i, source) => { - const prev = source[i - 1]; - if (prev != '.') return null; - - const match = code.match(/^[a-zA-Z0-9_-]+/); - if (!match) return null; - - return { - html: `${match[0]}`, - next: match[0].length - }; - }, - - // keyword - (code, i, source) => { - const prev = source[i - 1]; - if (prev && /[a-zA-Z]/.test(prev)) return null; - - const match = keywords.filter(k => code.substr(0, k.length) == k)[0]; - if (match) { - if (/^[a-zA-Z]/.test(code.substr(match.length))) return null; - return { - html: `${match}`, - next: match.length - }; - } else { - return null; - } - }, - - // symbol - code => { - const match = symbols.filter(s => code[0] == s)[0]; - if (match) { - return { - html: `${match}`, - next: 1 - }; - } else { - return null; - } - } -]; - -// TODO: specify lang -export default (source: string, lang?: string): string => { - let code = source; - let html = ''; - - let i = 0; - - function push(token: Token) { - html += token.html; - code = code.substr(token.next); - i += token.next; - } - - while (code != '') { - const parsed = elements.some(el => { - const e = el(code, i, source); - if (e) { - push(e); - return true; - } else { - return false; - } - }); - - if (!parsed) { - push({ - html: escape(code[0]), - next: 1 - }); - } - } - - return html; -};