parent
f9f70d5df4
commit
4b191c7f68
7 changed files with 76 additions and 372 deletions
|
@ -7,6 +7,7 @@ unreleased
|
||||||
* 外部サービス認証情報の配信
|
* 外部サービス認証情報の配信
|
||||||
* 管理画面のモデレーションのUIを強化
|
* 管理画面のモデレーションのUIを強化
|
||||||
* 管理画面からリモートユーザーの情報を更新できるように
|
* 管理画面からリモートユーザーの情報を更新できるように
|
||||||
|
* シンタックスハイライトの強化
|
||||||
* 引用投稿を削除したとき単なるRenoteとしてタイムラインに残る問題を修正
|
* 引用投稿を削除したとき単なるRenoteとしてタイムラインに残る問題を修正
|
||||||
* イタリック構文の判定の改善
|
* イタリック構文の判定の改善
|
||||||
* タイトル構文の判定の改善
|
* タイトル構文の判定の改善
|
||||||
|
|
|
@ -97,8 +97,8 @@
|
||||||
"bootstrap-vue": "2.0.0-rc.11",
|
"bootstrap-vue": "2.0.0-rc.11",
|
||||||
"cafy": "12.0.0",
|
"cafy": "12.0.0",
|
||||||
"chai": "4.2.0",
|
"chai": "4.2.0",
|
||||||
"chalk": "2.4.2",
|
|
||||||
"chai-http": "4.2.1",
|
"chai-http": "4.2.1",
|
||||||
|
"chalk": "2.4.2",
|
||||||
"commander": "2.19.0",
|
"commander": "2.19.0",
|
||||||
"crc-32": "1.2.0",
|
"crc-32": "1.2.0",
|
||||||
"css-loader": "1.0.1",
|
"css-loader": "1.0.1",
|
||||||
|
@ -178,6 +178,7 @@
|
||||||
"parsimmon": "1.12.0",
|
"parsimmon": "1.12.0",
|
||||||
"portscanner": "2.2.0",
|
"portscanner": "2.2.0",
|
||||||
"postcss-loader": "3.0.0",
|
"postcss-loader": "3.0.0",
|
||||||
|
"prismjs": "1.15.0",
|
||||||
"progress-bar-webpack-plugin": "1.12.0",
|
"progress-bar-webpack-plugin": "1.12.0",
|
||||||
"promise-any": "0.2.0",
|
"promise-any": "0.2.0",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
|
@ -230,6 +231,7 @@
|
||||||
"vue-js-modal": "1.3.28",
|
"vue-js-modal": "1.3.28",
|
||||||
"vue-loader": "15.5.1",
|
"vue-loader": "15.5.1",
|
||||||
"vue-marquee-text-component": "1.1.1",
|
"vue-marquee-text-component": "1.1.1",
|
||||||
|
"vue-prism-component": "1.1.1",
|
||||||
"vue-router": "3.0.2",
|
"vue-router": "3.0.2",
|
||||||
"vue-sequential-entrance": "1.1.3",
|
"vue-sequential-entrance": "1.1.3",
|
||||||
"vue-style-loader": "4.1.2",
|
"vue-style-loader": "4.1.2",
|
||||||
|
|
30
src/client/app/common/views/components/code-core.vue
Normal file
30
src/client/app/common/views/components/code-core.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<prism :inline="inline" :language="lang">{{ code }}</prism>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import 'prismjs';
|
||||||
|
import 'prismjs/themes/prism.css';
|
||||||
|
import Prism from 'vue-prism-component';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
Prism
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
code: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
lang: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
inline: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
28
src/client/app/common/views/components/code.vue
Normal file
28
src/client/app/common/views/components/code.vue
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<x-code :code="code" :lang="lang" :inline="inline"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
XCode: () => import('./code-core.vue').then(m => m.default)
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
code: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
lang: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
inline: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -6,8 +6,8 @@ import MkUrl from './url.vue';
|
||||||
import MkMention from './mention.vue';
|
import MkMention from './mention.vue';
|
||||||
import { concat, sum } from '../../../../../prelude/array';
|
import { concat, sum } from '../../../../../prelude/array';
|
||||||
import MkFormula from './formula.vue';
|
import MkFormula from './formula.vue';
|
||||||
|
import MkCode from './code.vue';
|
||||||
import MkGoogle from './google.vue';
|
import MkGoogle from './google.vue';
|
||||||
import syntaxHighlight from '../../../../../mfm/syntax-highlight';
|
|
||||||
import { host } from '../../../config';
|
import { host } from '../../../config';
|
||||||
import { preorderF, countNodesF } from '../../../../../prelude/tree';
|
import { preorderF, countNodesF } from '../../../../../prelude/tree';
|
||||||
|
|
||||||
|
@ -170,21 +170,22 @@ export default Vue.component('misskey-flavored-markdown', {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'blockCode': {
|
case 'blockCode': {
|
||||||
return [createElement('pre', {
|
return [createElement(MkCode, {
|
||||||
class: 'code'
|
key: Math.random(),
|
||||||
}, [
|
props: {
|
||||||
createElement('code', {
|
code: token.node.props.code,
|
||||||
domProps: {
|
lang: token.node.props.lang,
|
||||||
innerHTML: syntaxHighlight(token.node.props.code)
|
}
|
||||||
}
|
})];
|
||||||
})
|
|
||||||
])];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'inlineCode': {
|
case 'inlineCode': {
|
||||||
return [createElement('code', {
|
return [createElement(MkCode, {
|
||||||
domProps: {
|
key: Math.random(),
|
||||||
innerHTML: syntaxHighlight(token.node.props.code)
|
props: {
|
||||||
|
code: token.node.props.code,
|
||||||
|
lang: token.node.props.lang,
|
||||||
|
inline: true
|
||||||
}
|
}
|
||||||
})];
|
})];
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,25 +24,10 @@ export default Vue.extend({
|
||||||
background var(--mfmTitleBg)
|
background var(--mfmTitleBg)
|
||||||
border-radius 4px
|
border-radius 4px
|
||||||
|
|
||||||
>>> .code
|
|
||||||
margin 8px 0
|
|
||||||
|
|
||||||
>>> .quote
|
>>> .quote
|
||||||
margin 8px
|
margin 8px
|
||||||
padding 6px 0 6px 12px
|
padding 6px 0 6px 12px
|
||||||
color var(--mfmQuote)
|
color var(--mfmQuote)
|
||||||
border-left solid 3px var(--mfmQuoteLine)
|
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
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,343 +0,0 @@
|
||||||
import { capitalize, toUpperCase } from '../prelude/string';
|
|
||||||
|
|
||||||
function escape(text: string) {
|
|
||||||
return text
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/</g, '<');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 文字数が多い順にソートします
|
|
||||||
// そうしないと、「function」という文字列が与えられたときに「func」が先にマッチしてしまう可能性があるためです
|
|
||||||
const _keywords = [
|
|
||||||
'true',
|
|
||||||
'false',
|
|
||||||
'null',
|
|
||||||
'nil',
|
|
||||||
'undefined',
|
|
||||||
'void',
|
|
||||||
'var',
|
|
||||||
'const',
|
|
||||||
'let',
|
|
||||||
'mut',
|
|
||||||
'dim',
|
|
||||||
'if',
|
|
||||||
'then',
|
|
||||||
'else',
|
|
||||||
'switch',
|
|
||||||
'match',
|
|
||||||
'case',
|
|
||||||
'default',
|
|
||||||
'for',
|
|
||||||
'each',
|
|
||||||
'in',
|
|
||||||
'while',
|
|
||||||
'loop',
|
|
||||||
'continue',
|
|
||||||
'break',
|
|
||||||
'do',
|
|
||||||
'goto',
|
|
||||||
'next',
|
|
||||||
'end',
|
|
||||||
'sub',
|
|
||||||
'throw',
|
|
||||||
'try',
|
|
||||||
'catch',
|
|
||||||
'finally',
|
|
||||||
'enum',
|
|
||||||
'delegate',
|
|
||||||
'function',
|
|
||||||
'func',
|
|
||||||
'fun',
|
|
||||||
'fn',
|
|
||||||
'return',
|
|
||||||
'yield',
|
|
||||||
'async',
|
|
||||||
'await',
|
|
||||||
'require',
|
|
||||||
'include',
|
|
||||||
'import',
|
|
||||||
'imports',
|
|
||||||
'export',
|
|
||||||
'exports',
|
|
||||||
'from',
|
|
||||||
'as',
|
|
||||||
'using',
|
|
||||||
'use',
|
|
||||||
'internal',
|
|
||||||
'module',
|
|
||||||
'namespace',
|
|
||||||
'where',
|
|
||||||
'select',
|
|
||||||
'struct',
|
|
||||||
'union',
|
|
||||||
'new',
|
|
||||||
'delete',
|
|
||||||
'this',
|
|
||||||
'super',
|
|
||||||
'base',
|
|
||||||
'class',
|
|
||||||
'interface',
|
|
||||||
'abstract',
|
|
||||||
'static',
|
|
||||||
'public',
|
|
||||||
'private',
|
|
||||||
'protected',
|
|
||||||
'virtual',
|
|
||||||
'partial',
|
|
||||||
'override',
|
|
||||||
'extends',
|
|
||||||
'implements',
|
|
||||||
'constructor'
|
|
||||||
];
|
|
||||||
|
|
||||||
const keywords = _keywords
|
|
||||||
.concat(_keywords.map(capitalize))
|
|
||||||
.concat(_keywords.map(toUpperCase))
|
|
||||||
.sort((a, b) => 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: `<span class="comment">${escape(comment)}</span>`,
|
|
||||||
next: comment.length
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// block comment
|
|
||||||
code => {
|
|
||||||
const match = code.match(/^\/\*([\s\S]+?)\*\//);
|
|
||||||
if (!match) return null;
|
|
||||||
return {
|
|
||||||
html: `<span class="comment">${escape(match[0])}</span>`,
|
|
||||||
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: `<span class="string">${escape(str)}</span>`,
|
|
||||||
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: `<span class="regexp">/${escape(regexp)}/</span>`,
|
|
||||||
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: `<span class="label">${label}</span>`,
|
|
||||||
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: `<span class="number">${match}</span>`,
|
|
||||||
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: `<span class="nan">NaN</span>`,
|
|
||||||
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: `<span class="method">${match[1]}</span>`,
|
|
||||||
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: `<span class="property">${match[0]}</span>`,
|
|
||||||
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: `<span class="keyword ${match}">${match}</span>`,
|
|
||||||
next: match.length
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// symbol
|
|
||||||
code => {
|
|
||||||
const match = symbols.filter(s => code[0] == s)[0];
|
|
||||||
if (match) {
|
|
||||||
return {
|
|
||||||
html: `<span class="symbol">${match}</span>`,
|
|
||||||
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;
|
|
||||||
};
|
|
Loading…
Reference in a new issue