diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2017-03-18 20:05:11 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2017-03-18 20:05:11 +0900 |
| commit | 45e8331e261244628b134a18e3d0fbe0ebb3a7dc (patch) | |
| tree | 44ac1719fcea0a61c33698b23fb89400141e00d9 /src/api | |
| parent | Better notification (diff) | |
| download | sharkey-45e8331e261244628b134a18e3d0fbe0ebb3a7dc.tar.gz sharkey-45e8331e261244628b134a18e3d0fbe0ebb3a7dc.tar.bz2 sharkey-45e8331e261244628b134a18e3d0fbe0ebb3a7dc.zip | |
:sushi:
Closes #12, #227 and #58
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/common/text/core/syntax-highlighter.ts | 334 | ||||
| -rw-r--r-- | src/api/common/text/elements/bold.ts | 14 | ||||
| -rw-r--r-- | src/api/common/text/elements/code.ts | 17 | ||||
| -rw-r--r-- | src/api/common/text/elements/emoji.ts | 14 | ||||
| -rw-r--r-- | src/api/common/text/elements/hashtag.ts | 19 | ||||
| -rw-r--r-- | src/api/common/text/elements/inline-code.ts | 17 | ||||
| -rw-r--r-- | src/api/common/text/elements/link.ts | 19 | ||||
| -rw-r--r-- | src/api/common/text/elements/mention.ts | 14 | ||||
| -rw-r--r-- | src/api/common/text/elements/url.ts | 14 | ||||
| -rw-r--r-- | src/api/common/text/index.ts | 71 | ||||
| -rw-r--r-- | src/api/serializers/app.ts | 2 | ||||
| -rw-r--r-- | src/api/serializers/auth-session.ts | 2 | ||||
| -rw-r--r-- | src/api/serializers/drive-file.ts | 2 | ||||
| -rw-r--r-- | src/api/serializers/drive-folder.ts | 2 | ||||
| -rw-r--r-- | src/api/serializers/drive-tag.ts | 2 | ||||
| -rw-r--r-- | src/api/serializers/messaging-message.ts | 10 | ||||
| -rw-r--r-- | src/api/serializers/notification.ts | 2 | ||||
| -rw-r--r-- | src/api/serializers/post.ts | 10 | ||||
| -rw-r--r-- | src/api/serializers/signin.ts | 2 | ||||
| -rw-r--r-- | src/api/serializers/user.ts | 2 |
20 files changed, 547 insertions, 22 deletions
diff --git a/src/api/common/text/core/syntax-highlighter.ts b/src/api/common/text/core/syntax-highlighter.ts new file mode 100644 index 0000000000..c0396b1fc6 --- /dev/null +++ b/src/api/common/text/core/syntax-highlighter.ts @@ -0,0 +1,334 @@ +function escape(text) { + 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(k => k[0].toUpperCase() + k.substr(1))) + .concat(_keywords.map(k => k.toUpperCase())) + .sort((a, b) => b.length - a.length); + +const symbols = [ + '=', + '+', + '-', + '*', + '/', + '%', + '~', + '^', + '&', + '|', + '>', + '<', + '!', + '?' +]; + +const elements = [ + // 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[0] == ' ' && regexp[regexp.length - 1] == ' ') 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; + } + } +]; + +// specify lang is todo +export default (source: string, lang?: string) => { + let code = source; + let html = ''; + + let i = 0; + + function push(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; +}; diff --git a/src/api/common/text/elements/bold.ts b/src/api/common/text/elements/bold.ts new file mode 100644 index 0000000000..ce25764457 --- /dev/null +++ b/src/api/common/text/elements/bold.ts @@ -0,0 +1,14 @@ +/** + * Bold + */ + +module.exports = text => { + const match = text.match(/^\*\*(.+?)\*\*/); + if (!match) return null; + const bold = match[0]; + return { + type: 'bold', + content: bold, + bold: bold.substr(2, bold.length - 4) + }; +}; diff --git a/src/api/common/text/elements/code.ts b/src/api/common/text/elements/code.ts new file mode 100644 index 0000000000..4821e95fe2 --- /dev/null +++ b/src/api/common/text/elements/code.ts @@ -0,0 +1,17 @@ +/** + * Code (block) + */ + +import genHtml from '../core/syntax-highlighter'; + +module.exports = text => { + const match = text.match(/^```([\s\S]+?)```/); + if (!match) return null; + const code = match[0]; + return { + type: 'code', + content: code, + code: code.substr(3, code.length - 6).trim(), + html: genHtml(code.substr(3, code.length - 6).trim()) + }; +}; diff --git a/src/api/common/text/elements/emoji.ts b/src/api/common/text/elements/emoji.ts new file mode 100644 index 0000000000..e24231a223 --- /dev/null +++ b/src/api/common/text/elements/emoji.ts @@ -0,0 +1,14 @@ +/** + * Emoji + */ + +module.exports = text => { + const match = text.match(/^:[a-zA-Z0-9+-_]+:/); + if (!match) return null; + const emoji = match[0]; + return { + type: 'emoji', + content: emoji, + emoji: emoji.substr(1, emoji.length - 2) + }; +}; diff --git a/src/api/common/text/elements/hashtag.ts b/src/api/common/text/elements/hashtag.ts new file mode 100644 index 0000000000..048dbd8929 --- /dev/null +++ b/src/api/common/text/elements/hashtag.ts @@ -0,0 +1,19 @@ +/** + * Hashtag + */ + +module.exports = (text, i) => { + if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null; + const isHead = text[0] == '#'; + const hashtag = text.match(/^\s?#[^\s]+/)[0]; + const res = !isHead ? [{ + type: 'text', + content: text[0] + }] : []; + res.push({ + type: 'hashtag', + content: isHead ? hashtag : hashtag.substr(1), + hashtag: isHead ? hashtag.substr(1) : hashtag.substr(2) + }); + return res; +}; diff --git a/src/api/common/text/elements/inline-code.ts b/src/api/common/text/elements/inline-code.ts new file mode 100644 index 0000000000..9f9ef51a2b --- /dev/null +++ b/src/api/common/text/elements/inline-code.ts @@ -0,0 +1,17 @@ +/** + * Code (inline) + */ + +import genHtml from '../core/syntax-highlighter'; + +module.exports = text => { + const match = text.match(/^`(.+?)`/); + if (!match) return null; + const code = match[0]; + return { + type: 'inline-code', + content: code, + code: code.substr(1, code.length - 2).trim(), + html: genHtml(code.substr(1, code.length - 2).trim()) + }; +}; diff --git a/src/api/common/text/elements/link.ts b/src/api/common/text/elements/link.ts new file mode 100644 index 0000000000..35563ddc3d --- /dev/null +++ b/src/api/common/text/elements/link.ts @@ -0,0 +1,19 @@ +/** + * Link + */ + +module.exports = text => { + const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/); + if (!match) return null; + const silent = text[0] == '?'; + const link = match[0]; + const title = match[1]; + const url = match[2]; + return { + type: 'link', + content: link, + title: title, + url: url, + silent: silent + }; +}; diff --git a/src/api/common/text/elements/mention.ts b/src/api/common/text/elements/mention.ts new file mode 100644 index 0000000000..e0fac4dd76 --- /dev/null +++ b/src/api/common/text/elements/mention.ts @@ -0,0 +1,14 @@ +/** + * Mention + */ + +module.exports = text => { + const match = text.match(/^@[a-zA-Z0-9\-]+/); + if (!match) return null; + const mention = match[0]; + return { + type: 'mention', + content: mention, + username: mention.substr(1) + }; +}; diff --git a/src/api/common/text/elements/url.ts b/src/api/common/text/elements/url.ts new file mode 100644 index 0000000000..1003aff9c3 --- /dev/null +++ b/src/api/common/text/elements/url.ts @@ -0,0 +1,14 @@ +/** + * URL + */ + +module.exports = text => { + const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+/); + if (!match) return null; + const url = match[0]; + return { + type: 'url', + content: url, + url: url + }; +}; diff --git a/src/api/common/text/index.ts b/src/api/common/text/index.ts new file mode 100644 index 0000000000..47127e8646 --- /dev/null +++ b/src/api/common/text/index.ts @@ -0,0 +1,71 @@ +/** + * Misskey Text Analyzer + */ + +const elements = [ + require('./elements/bold'), + require('./elements/url'), + require('./elements/link'), + require('./elements/mention'), + require('./elements/hashtag'), + require('./elements/code'), + require('./elements/inline-code'), + require('./elements/emoji') +]; + +export default (source: string) => { + + if (source == '') { + return null; + } + + const tokens = []; + + function push(token) { + if (token != null) { + tokens.push(token); + source = source.substr(token.content.length); + } + } + + let i = 0; + + // パース + while (source != '') { + const parsed = elements.some(el => { + let tokens = el(source, i); + if (tokens) { + if (!Array.isArray(tokens)) { + tokens = [tokens]; + } + tokens.forEach(push); + return true; + } else { + return false; + } + }); + + if (!parsed) { + push({ + type: 'text', + content: source[0] + }); + } + + i++; + } + + // テキストを纏める + tokens[0] = [tokens[0]]; + return tokens.reduce((a, b) => { + if (a[a.length - 1].type == 'text' && b.type == 'text') { + const tail = a.pop(); + return a.concat({ + type: 'text', + content: tail.content + b.content + }); + } else { + return a.concat(b); + } + }); +}; diff --git a/src/api/serializers/app.ts b/src/api/serializers/app.ts index fdeef338d9..9d1c46dca4 100644 --- a/src/api/serializers/app.ts +++ b/src/api/serializers/app.ts @@ -1,5 +1,3 @@ -'use strict'; - /** * Module dependencies */ diff --git a/src/api/serializers/auth-session.ts b/src/api/serializers/auth-session.ts index 4efb7729c4..a9acf1243a 100644 --- a/src/api/serializers/auth-session.ts +++ b/src/api/serializers/auth-session.ts @@ -1,5 +1,3 @@ -'use strict'; - /** * Module dependencies */ diff --git a/src/api/serializers/drive-file.ts b/src/api/serializers/drive-file.ts index e6e2f6cae3..b4e2ab064a 100644 --- a/src/api/serializers/drive-file.ts +++ b/src/api/serializers/drive-file.ts @@ -1,5 +1,3 @@ -'use strict'; - /** * Module dependencies */ diff --git a/src/api/serializers/drive-folder.ts b/src/api/serializers/drive-folder.ts index ac3bd13c3a..34fdc0d905 100644 --- a/src/api/serializers/drive-folder.ts +++ b/src/api/serializers/drive-folder.ts @@ -1,5 +1,3 @@ -'use strict'; - /** * Module dependencies */ diff --git a/src/api/serializers/drive-tag.ts b/src/api/serializers/drive-tag.ts index 3e800ca5bd..2f152381bd 100644 --- a/src/api/serializers/drive-tag.ts +++ b/src/api/serializers/drive-tag.ts @@ -1,5 +1,3 @@ -'use strict'; - /** * Module dependencies */ diff --git a/src/api/serializers/messaging-message.ts b/src/api/serializers/messaging-message.ts index da63f8b99e..4ab95e42a3 100644 --- a/src/api/serializers/messaging-message.ts +++ b/src/api/serializers/messaging-message.ts @@ -1,13 +1,12 @@ -'use strict'; - /** * Module dependencies */ import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); import Message from '../models/messaging-message'; import serializeUser from './user'; import serializeDriveFile from './drive-file'; -import deepcopy = require('deepcopy'); +import parse from '../common/text'; /** * Serialize a message @@ -47,6 +46,11 @@ export default ( _message.id = _message._id; delete _message._id; + // Parse text + if (_message.text) { + _message.ast = parse(_message.text); + } + // Populate user _message.user = await serializeUser(_message.user_id, me); diff --git a/src/api/serializers/notification.ts b/src/api/serializers/notification.ts index 43add127e0..50952e5426 100644 --- a/src/api/serializers/notification.ts +++ b/src/api/serializers/notification.ts @@ -1,5 +1,3 @@ -'use strict'; - /** * Module dependencies */ diff --git a/src/api/serializers/post.ts b/src/api/serializers/post.ts index b71b42e9a4..f459529697 100644 --- a/src/api/serializers/post.ts +++ b/src/api/serializers/post.ts @@ -1,16 +1,15 @@ -'use strict'; - /** * Module dependencies */ import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); import Post from '../models/post'; import Like from '../models/like'; import Vote from '../models/poll-vote'; import serializeApp from './app'; import serializeUser from './user'; import serializeDriveFile from './drive-file'; -import deepcopy = require('deepcopy'); +import parse from '../common/text'; /** * Serialize a post @@ -54,6 +53,11 @@ const self = ( delete _post.mentions; + // Parse text + if (_post.text) { + _post.ast = parse(_post.text); + } + // Populate user _post.user = await serializeUser(_post.user_id, me); diff --git a/src/api/serializers/signin.ts b/src/api/serializers/signin.ts index 39226f8bd4..4068067678 100644 --- a/src/api/serializers/signin.ts +++ b/src/api/serializers/signin.ts @@ -1,5 +1,3 @@ -'use strict'; - /** * Module dependencies */ diff --git a/src/api/serializers/user.ts b/src/api/serializers/user.ts index de215808a4..d367dc8657 100644 --- a/src/api/serializers/user.ts +++ b/src/api/serializers/user.ts @@ -1,5 +1,3 @@ -'use strict'; - /** * Module dependencies */ |