summaryrefslogtreecommitdiff
path: root/src/api
diff options
context:
space:
mode:
authorsyuilo <syuilotan@yahoo.co.jp>2017-03-18 20:05:11 +0900
committersyuilo <syuilotan@yahoo.co.jp>2017-03-18 20:05:11 +0900
commit45e8331e261244628b134a18e3d0fbe0ebb3a7dc (patch)
tree44ac1719fcea0a61c33698b23fb89400141e00d9 /src/api
parentBetter notification (diff)
downloadsharkey-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.ts334
-rw-r--r--src/api/common/text/elements/bold.ts14
-rw-r--r--src/api/common/text/elements/code.ts17
-rw-r--r--src/api/common/text/elements/emoji.ts14
-rw-r--r--src/api/common/text/elements/hashtag.ts19
-rw-r--r--src/api/common/text/elements/inline-code.ts17
-rw-r--r--src/api/common/text/elements/link.ts19
-rw-r--r--src/api/common/text/elements/mention.ts14
-rw-r--r--src/api/common/text/elements/url.ts14
-rw-r--r--src/api/common/text/index.ts71
-rw-r--r--src/api/serializers/app.ts2
-rw-r--r--src/api/serializers/auth-session.ts2
-rw-r--r--src/api/serializers/drive-file.ts2
-rw-r--r--src/api/serializers/drive-folder.ts2
-rw-r--r--src/api/serializers/drive-tag.ts2
-rw-r--r--src/api/serializers/messaging-message.ts10
-rw-r--r--src/api/serializers/notification.ts2
-rw-r--r--src/api/serializers/post.ts10
-rw-r--r--src/api/serializers/signin.ts2
-rw-r--r--src/api/serializers/user.ts2
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, '&gt;')
+ .replace(/</g, '&lt;');
+}
+
+// 文字数が多い順にソートします
+// そうしないと、「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
*/