From 45e8331e261244628b134a18e3d0fbe0ebb3a7dc Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 18 Mar 2017 20:05:11 +0900 Subject: :sushi: Closes #12, #227 and #58 --- src/api/common/text/core/syntax-highlighter.ts | 334 +++++++++++++++++++++++++ src/api/common/text/elements/bold.ts | 14 ++ src/api/common/text/elements/code.ts | 17 ++ src/api/common/text/elements/emoji.ts | 14 ++ src/api/common/text/elements/hashtag.ts | 19 ++ src/api/common/text/elements/inline-code.ts | 17 ++ src/api/common/text/elements/link.ts | 19 ++ src/api/common/text/elements/mention.ts | 14 ++ src/api/common/text/elements/url.ts | 14 ++ src/api/common/text/index.ts | 71 ++++++ src/api/serializers/app.ts | 2 - src/api/serializers/auth-session.ts | 2 - src/api/serializers/drive-file.ts | 2 - src/api/serializers/drive-folder.ts | 2 - src/api/serializers/drive-tag.ts | 2 - src/api/serializers/messaging-message.ts | 10 +- src/api/serializers/notification.ts | 2 - src/api/serializers/post.ts | 10 +- src/api/serializers/signin.ts | 2 - src/api/serializers/user.ts | 2 - 20 files changed, 547 insertions(+), 22 deletions(-) create mode 100644 src/api/common/text/core/syntax-highlighter.ts create mode 100644 src/api/common/text/elements/bold.ts create mode 100644 src/api/common/text/elements/code.ts create mode 100644 src/api/common/text/elements/emoji.ts create mode 100644 src/api/common/text/elements/hashtag.ts create mode 100644 src/api/common/text/elements/inline-code.ts create mode 100644 src/api/common/text/elements/link.ts create mode 100644 src/api/common/text/elements/mention.ts create mode 100644 src/api/common/text/elements/url.ts create mode 100644 src/api/common/text/index.ts (limited to 'src/api') 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(/ 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: `${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[0] == ' ' && regexp[regexp.length - 1] == ' ') 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; + } + } +]; + +// 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 */ -- cgit v1.2.3-freya