summaryrefslogtreecommitdiff
path: root/src/mfm
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2018-11-21 05:11:00 +0900
committerGitHub <noreply@github.com>2018-11-21 05:11:00 +0900
commit79ffbf95db9d0cc019d06ab93b1bfa6ba0d4f9ae (patch)
tree884a18e6735f8557eb392929fcfa2dc1ae09cb6d /src/mfm
parentRefactor checkMongoDb (#3339) (diff)
downloadsharkey-79ffbf95db9d0cc019d06ab93b1bfa6ba0d4f9ae.tar.gz
sharkey-79ffbf95db9d0cc019d06ab93b1bfa6ba0d4f9ae.tar.bz2
sharkey-79ffbf95db9d0cc019d06ab93b1bfa6ba0d4f9ae.zip
Improve MFM parser (#3337)
* wip * wip * Refactor * Refactor * wip * wip * wip * wip * Refactor * Refactor * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Clean up * Update misskey-flavored-markdown.ts * wip * wip * wip * wip * Update parser.ts * wip * Add new test * wip * Add new test * Add new test * wip * Refactor * Update parse.ts * Refactor * Update parser.ts * wip
Diffstat (limited to 'src/mfm')
-rw-r--r--src/mfm/html.ts212
-rw-r--r--src/mfm/parse.ts81
-rw-r--r--src/mfm/parse/elements/big.ts20
-rw-r--r--src/mfm/parse/elements/bold.ts20
-rw-r--r--src/mfm/parse/elements/code.ts24
-rw-r--r--src/mfm/parse/elements/emoji.regex.ts2
-rw-r--r--src/mfm/parse/elements/emoji.ts33
-rw-r--r--src/mfm/parse/elements/hashtag.ts27
-rw-r--r--src/mfm/parse/elements/inline-code.ts25
-rw-r--r--src/mfm/parse/elements/link.ts27
-rw-r--r--src/mfm/parse/elements/math.ts20
-rw-r--r--src/mfm/parse/elements/mention.ts29
-rw-r--r--src/mfm/parse/elements/motion.ts20
-rw-r--r--src/mfm/parse/elements/quote.ts30
-rw-r--r--src/mfm/parse/elements/search.ts19
-rw-r--r--src/mfm/parse/elements/title.ts21
-rw-r--r--src/mfm/parse/elements/url.ts23
-rw-r--r--src/mfm/parse/index.ts100
-rw-r--r--src/mfm/parser.ts256
-rw-r--r--src/mfm/syntax-highlight.ts (renamed from src/mfm/parse/core/syntax-highlighter.ts)4
20 files changed, 449 insertions, 544 deletions
diff --git a/src/mfm/html.ts b/src/mfm/html.ts
index cb7c7e2855..d45cc13af4 100644
--- a/src/mfm/html.ts
+++ b/src/mfm/html.ts
@@ -1,127 +1,135 @@
-const { lib: emojilib } = require('emojilib');
const jsdom = require('jsdom');
const { JSDOM } = jsdom;
import config from '../config';
import { INote } from '../models/note';
-import { TextElement } from './parse';
+import { Node } from './parser';
import { intersperse } from '../prelude/array';
-const handlers: { [key: string]: (window: any, token: any, mentionedRemoteUsers: INote['mentionedRemoteUsers']) => void } = {
- bold({ document }, { bold }) {
- const b = document.createElement('b');
- b.textContent = bold;
- document.body.appendChild(b);
- },
+export default (tokens: Node[], mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) => {
+ if (tokens == null) {
+ return null;
+ }
- big({ document }, { big }) {
- const b = document.createElement('strong');
- b.textContent = big;
- document.body.appendChild(b);
- },
+ const { window } = new JSDOM('');
- motion({ document }, { big }) {
- const b = document.createElement('strong');
- b.textContent = big;
- document.body.appendChild(b);
- },
+ const doc = window.document;
- code({ document }, { code }) {
- const pre = document.createElement('pre');
- const inner = document.createElement('code');
- inner.innerHTML = code;
- pre.appendChild(inner);
- document.body.appendChild(pre);
- },
+ function dive(nodes: Node[]): any[] {
+ return nodes.map(n => handlers[n.name](n));
+ }
- emoji({ document }, { content, emoji }) {
- const found = emojilib[emoji];
- const node = document.createTextNode(found ? found.char : content);
- document.body.appendChild(node);
- },
+ const handlers: { [key: string]: (token: Node) => any } = {
+ bold(token) {
+ const el = doc.createElement('b');
+ dive(token.children).forEach(child => el.appendChild(child));
+ return el;
+ },
- hashtag({ document }, { hashtag }) {
- const a = document.createElement('a');
- a.href = `${config.url}/tags/${hashtag}`;
- a.textContent = `#${hashtag}`;
- a.setAttribute('rel', 'tag');
- document.body.appendChild(a);
- },
+ big(token) {
+ const el = doc.createElement('strong');
+ dive(token.children).forEach(child => el.appendChild(child));
+ return el;
+ },
- 'inline-code'({ document }, { code }) {
- const element = document.createElement('code');
- element.textContent = code;
- document.body.appendChild(element);
- },
+ motion(token) {
+ const el = doc.createElement('i');
+ dive(token.children).forEach(child => el.appendChild(child));
+ return el;
+ },
- math({ document }, { formula }) {
- const element = document.createElement('code');
- element.textContent = formula;
- document.body.appendChild(element);
- },
+ blockCode(token) {
+ const pre = doc.createElement('pre');
+ const inner = doc.createElement('code');
+ inner.innerHTML = token.props.code;
+ pre.appendChild(inner);
+ return pre;
+ },
- link({ document }, { url, title }) {
- const a = document.createElement('a');
- a.href = url;
- a.textContent = title;
- document.body.appendChild(a);
- },
+ emoji(token) {
+ return doc.createTextNode(token.props.emoji ? token.props.emoji : `:${token.props.name}:`);
+ },
- mention({ document }, { content, username, host }, mentionedRemoteUsers) {
- const a = document.createElement('a');
- const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
- a.href = remoteUserInfo ? remoteUserInfo.uri : `${config.url}/${content}`;
- a.textContent = content;
- document.body.appendChild(a);
- },
+ hashtag(token) {
+ const a = doc.createElement('a');
+ a.href = `${config.url}/tags/${token.props.hashtag}`;
+ a.textContent = `#${token.props.hashtag}`;
+ a.setAttribute('rel', 'tag');
+ return a;
+ },
- quote({ document }, { quote }) {
- const blockquote = document.createElement('blockquote');
- blockquote.textContent = quote;
- document.body.appendChild(blockquote);
- },
+ inlineCode(token) {
+ const el = doc.createElement('code');
+ el.textContent = token.props.code;
+ return el;
+ },
- title({ document }, { content }) {
- const h1 = document.createElement('h1');
- h1.textContent = content;
- document.body.appendChild(h1);
- },
+ math(token) {
+ const el = doc.createElement('code');
+ el.textContent = token.props.formula;
+ return el;
+ },
- text({ document }, { content }) {
- const nodes = (content as string).split('\n').map(x => document.createTextNode(x));
- for (const x of intersperse('br', nodes)) {
- if (x === 'br') {
- document.body.appendChild(document.createElement('br'));
- } else {
- document.body.appendChild(x);
- }
- }
- },
+ link(token) {
+ const a = doc.createElement('a');
+ a.href = token.props.url;
+ dive(token.children).forEach(child => a.appendChild(child));
+ return a;
+ },
- url({ document }, { url }) {
- const a = document.createElement('a');
- a.href = url;
- a.textContent = url;
- document.body.appendChild(a);
- },
+ mention(token) {
+ const a = doc.createElement('a');
+ const { username, host, acct } = token.props;
+ const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
+ a.href = remoteUserInfo ? remoteUserInfo.uri : `${config.url}/${acct}`;
+ a.textContent = acct;
+ return a;
+ },
- search({ document }, { content, query }) {
- const a = document.createElement('a');
- a.href = `https://www.google.com/?#q=${query}`;
- a.textContent = content;
- document.body.appendChild(a);
- }
-};
+ quote(token) {
+ const el = doc.createElement('blockquote');
+ dive(token.children).forEach(child => el.appendChild(child));
+ return el;
+ },
-export default (tokens: TextElement[], mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) => {
- if (tokens == null) {
- return null;
- }
+ title(token) {
+ const el = doc.createElement('h1');
+ dive(token.children).forEach(child => el.appendChild(child));
+ return el;
+ },
- const { window } = new JSDOM('');
+ text(token) {
+ const el = doc.createElement('span');
+ const nodes = (token.props.text as string).split('\n').map(x => doc.createTextNode(x));
- for (const token of tokens) {
- handlers[token.type](window, token, mentionedRemoteUsers);
- }
+ for (const x of intersperse('br', nodes)) {
+ if (x === 'br') {
+ el.appendChild(doc.createElement('br'));
+ } else {
+ el.appendChild(x);
+ }
+ }
+
+ return el;
+ },
+
+ url(token) {
+ const a = doc.createElement('a');
+ a.href = token.props.url;
+ a.textContent = token.props.url;
+ return a;
+ },
+
+ search(token) {
+ const a = doc.createElement('a');
+ a.href = `https://www.google.com/?#q=${token.props.query}`;
+ a.textContent = token.props.content;
+ return a;
+ }
+ };
+
+ dive(tokens).forEach(x => {
+ doc.body.appendChild(x);
+ });
- return `<p>${window.document.body.innerHTML}</p>`;
+ return `<p>${doc.body.innerHTML}</p>`;
};
diff --git a/src/mfm/parse.ts b/src/mfm/parse.ts
new file mode 100644
index 0000000000..e7ae3d012e
--- /dev/null
+++ b/src/mfm/parse.ts
@@ -0,0 +1,81 @@
+import parser, { Node } from './parser';
+import * as A from '../prelude/array';
+import * as S from '../prelude/string';
+
+export default (source: string): Node[] => {
+ if (source == null || source == '') {
+ return null;
+ }
+
+ let nodes: Node[] = parser.root.tryParse(source);
+
+ const combineText = (es: Node[]): Node =>
+ ({ name: 'text', props: { text: S.concat(es.map(e => e.props.text)) } });
+
+ const concatText = (nodes: Node[]): Node[] =>
+ A.concat(A.groupOn(x => x.name, nodes).map(es =>
+ es[0].name === 'text' ? [combineText(es)] : es
+ ));
+
+ const concatTextRecursive = (es: Node[]): void =>
+ es.filter(x => x.children).forEach(x => {
+ x.children = concatText(x.children);
+ concatTextRecursive(x.children);
+ });
+
+ nodes = concatText(nodes);
+ concatTextRecursive(nodes);
+
+ function getBeforeTextNode(node: Node): Node {
+ if (node == null) return null;
+ if (node.name == 'text') return node;
+ if (node.children) return getBeforeTextNode(node.children[node.children.length - 1]);
+ return null;
+ }
+
+ function getAfterTextNode(node: Node): Node {
+ if (node == null) return null;
+ if (node.name == 'text') return node;
+ if (node.children) return getBeforeTextNode(node.children[0]);
+ return null;
+ }
+
+ function isBlockNode(node: Node): boolean {
+ return ['blockCode', 'quote', 'title'].includes(node.name);
+ }
+
+ /**
+ * ブロック要素の前後にある改行を削除します(ブロック要素自体が改行の役割も果たすため、余計に改行されてしまうため)
+ * @param nodes
+ */
+ const removeNeedlessLineBreaks = (nodes: Node[]) => {
+ nodes.forEach((node, i) => {
+ if (node.children) removeNeedlessLineBreaks(node.children);
+ if (isBlockNode(node)) {
+ const before = getBeforeTextNode(nodes[i - 1]);
+ const after = getAfterTextNode(nodes[i + 1]);
+ if (before && before.props.text.endsWith('\n')) {
+ before.props.text = before.props.text.substring(0, before.props.text.length - 1);
+ }
+ if (after && after.props.text.startsWith('\n')) {
+ after.props.text = after.props.text.substring(1);
+ }
+ }
+ });
+ };
+
+ const removeEmptyTextNodes = (nodes: Node[]) => {
+ nodes.forEach(n => {
+ if (n.children) {
+ n.children = removeEmptyTextNodes(n.children);
+ }
+ });
+ return nodes.filter(n => !(n.name == 'text' && n.props.text == ''));
+ };
+
+ removeNeedlessLineBreaks(nodes);
+
+ nodes = removeEmptyTextNodes(nodes);
+
+ return nodes;
+};
diff --git a/src/mfm/parse/elements/big.ts b/src/mfm/parse/elements/big.ts
deleted file mode 100644
index 24e8bad50e..0000000000
--- a/src/mfm/parse/elements/big.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Big
- */
-
-export type TextElementBig = {
- type: 'big';
- content: string;
- big: string;
-};
-
-export default function(text: string) {
- const match = text.match(/^\*\*\*(.+?)\*\*\*/);
- if (!match) return null;
- const big = match[0];
- return {
- type: 'big',
- content: big,
- big: match[1]
- } as TextElementBig;
-}
diff --git a/src/mfm/parse/elements/bold.ts b/src/mfm/parse/elements/bold.ts
deleted file mode 100644
index 42c9cf0e1e..0000000000
--- a/src/mfm/parse/elements/bold.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Bold
- */
-
-export type TextElementBold = {
- type: 'bold';
- content: string;
- bold: string;
-};
-
-export default function(text: string) {
- const match = text.match(/^\*\*(.+?)\*\*/);
- if (!match) return null;
- const bold = match[0];
- return {
- type: 'bold',
- content: bold,
- bold: match[1]
- } as TextElementBold;
-}
diff --git a/src/mfm/parse/elements/code.ts b/src/mfm/parse/elements/code.ts
deleted file mode 100644
index 63a535fc55..0000000000
--- a/src/mfm/parse/elements/code.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Code (block)
- */
-
-import genHtml from '../core/syntax-highlighter';
-
-export type TextElementCode = {
- type: 'code';
- content: string;
- code: string;
- html: string;
-};
-
-export default function(text: string) {
- const match = text.match(/^```([\s\S]+?)```/);
- if (!match) return null;
- const code = match[0];
- return {
- type: 'code',
- content: code,
- code: match[1],
- html: genHtml(match[1].trim())
- } as TextElementCode;
-}
diff --git a/src/mfm/parse/elements/emoji.regex.ts b/src/mfm/parse/elements/emoji.regex.ts
deleted file mode 100644
index a5c6b71825..0000000000
--- a/src/mfm/parse/elements/emoji.regex.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// https://github.com/twitter/twemoji/blob/fc458b467c1bd706acd8653028ee8ab3e6562ce3/2/scripts/regex
-export const emojiRegex = /^((?:\ud83d[\udc68\udc69])(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddb0-\uddb3])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f)|[\u0023\u002a\u0030-\u0039]\ufe0f?\u20e3|(?:[\u00a9\u00ae\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\uddb5\uddb6\uddb8\uddb9\uddd1-\udddd]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a-\udc6d\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\udeeb\udeec\udef4-\udef9]|\ud83e[\udd10-\udd17\udd1d\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd40-\udd45\udd47-\udd70\udd73-\udd76\udd7a\udd7c-\udda2\uddb4\uddb7\uddc0-\uddc2\uddd0\uddde-\uddff]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f)/;
diff --git a/src/mfm/parse/elements/emoji.ts b/src/mfm/parse/elements/emoji.ts
deleted file mode 100644
index 6c09ddf5c0..0000000000
--- a/src/mfm/parse/elements/emoji.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Emoji
- */
-
-import { emojiRegex } from "./emoji.regex";
-
-export type TextElementEmoji = {
- type: 'emoji';
- content: string;
- emoji?: string;
- name?: string;
-};
-
-export default function(text: string) {
- const name = text.match(/^:([a-zA-Z0-9+_-]+):/);
- if (name) {
- return {
- type: 'emoji',
- content: name[0],
- name: name[1]
- } as TextElementEmoji;
- }
- const unicode = text.match(emojiRegex);
- if (unicode) {
- const [content] = unicode;
- return {
- type: 'emoji',
- content,
- emoji: content
- } as TextElementEmoji;
- }
- return null;
-}
diff --git a/src/mfm/parse/elements/hashtag.ts b/src/mfm/parse/elements/hashtag.ts
deleted file mode 100644
index df07de6645..0000000000
--- a/src/mfm/parse/elements/hashtag.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Hashtag
- */
-
-export type TextElementHashtag = {
- type: 'hashtag';
- content: string;
- hashtag: string;
-};
-
-export default function(text: string, before: string) {
- const isBegin = before == '';
-
- if (!(/^\s#[^\s\.,!\?#]+/.test(text) || (isBegin && /^#[^\s\.,!\?#]+/.test(text)))) return null;
- const isHead = text.startsWith('#');
- const hashtag = text.match(/^\s?#[^\s\.,!\?#]+/)[0];
- const res: any[] = !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 as TextElementHashtag[];
-}
diff --git a/src/mfm/parse/elements/inline-code.ts b/src/mfm/parse/elements/inline-code.ts
deleted file mode 100644
index efacd734cb..0000000000
--- a/src/mfm/parse/elements/inline-code.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Code (inline)
- */
-
-import genHtml from '../core/syntax-highlighter';
-
-export type TextElementInlineCode = {
- type: 'inline-code';
- content: string;
- code: string;
- html: string;
-};
-
-export default function(text: string) {
- const match = text.match(/^`(.+?)`/);
- if (!match) return null;
- if (match[1].includes('´')) return null;
- const code = match[0];
- return {
- type: 'inline-code',
- content: code,
- code: match[1],
- html: genHtml(match[1])
- } as TextElementInlineCode;
-}
diff --git a/src/mfm/parse/elements/link.ts b/src/mfm/parse/elements/link.ts
deleted file mode 100644
index 972fce3810..0000000000
--- a/src/mfm/parse/elements/link.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Link
- */
-
-export type TextElementLink = {
- type: 'link';
- content: string;
- title: string;
- url: string;
- silent: boolean;
-};
-
-export default function(text: string) {
- const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/);
- if (!match) return null;
- const silent = text.startsWith('?');
- const link = match[0];
- const title = match[1];
- const url = match[2];
- return {
- type: 'link',
- content: link,
- title: title,
- url: url,
- silent: silent
- } as TextElementLink;
-}
diff --git a/src/mfm/parse/elements/math.ts b/src/mfm/parse/elements/math.ts
deleted file mode 100644
index f2b6c5f479..0000000000
--- a/src/mfm/parse/elements/math.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Math
- */
-
-export type TextElementMath = {
- type: 'math';
- content: string;
- formula: string;
-};
-
-export default function(text: string) {
- const match = text.match(/^\\\((.+?)\\\)/);
- if (!match) return null;
- const math = match[0];
- return {
- type: 'math',
- content: math,
- formula: match[1]
- } as TextElementMath;
-}
diff --git a/src/mfm/parse/elements/mention.ts b/src/mfm/parse/elements/mention.ts
deleted file mode 100644
index 7a609e5d34..0000000000
--- a/src/mfm/parse/elements/mention.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Mention
- */
-import parseAcct from '../../../misc/acct/parse';
-import { toUnicode } from 'punycode';
-
-export type TextElementMention = {
- type: 'mention';
- content: string;
- canonical: string;
- username: string;
- host: string;
-};
-
-export default function(text: string, before: string) {
- const match = text.match(/^@[a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9])?/i);
- if (!match) return null;
- if (/[a-zA-Z0-9]$/.test(before)) return null;
- const mention = match[0];
- const { username, host } = parseAcct(mention.substr(1));
- const canonical = host != null ? `@${username}@${toUnicode(host)}` : mention;
- return {
- type: 'mention',
- content: mention,
- canonical,
- username,
- host
- } as TextElementMention;
-}
diff --git a/src/mfm/parse/elements/motion.ts b/src/mfm/parse/elements/motion.ts
deleted file mode 100644
index c6500e7be0..0000000000
--- a/src/mfm/parse/elements/motion.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Motion
- */
-
-export type TextElementMotion = {
- type: 'motion';
- content: string;
- motion: string;
-};
-
-export default function(text: string) {
- const match = text.match(/^\(\(\((.+?)\)\)\)/) || text.match(/^<motion>(.+?)<\/motion>/);
- if (!match) return null;
- const motion = match[0];
- return {
- type: 'motion',
- content: motion,
- motion: match[1]
- } as TextElementMotion;
-}
diff --git a/src/mfm/parse/elements/quote.ts b/src/mfm/parse/elements/quote.ts
deleted file mode 100644
index 969c1fb4a9..0000000000
--- a/src/mfm/parse/elements/quote.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * Quoted text
- */
-
-export type TextElementQuote = {
- type: 'quote';
- content: string;
- quote: string;
-};
-
-export default function(text: string, before: string) {
- const isBegin = before == '';
-
- const match = text.match(/^"([\s\S]+?)\n"/) || text.match(/^\n>([\s\S]+?)(\n\n|$)/) ||
- (isBegin ? text.match(/^>([\s\S]+?)(\n\n|$)/) : null);
-
- if (!match) return null;
-
- const quote = match[1]
- .split('\n')
- .map(line => line.replace(/^>+/g, '').trim())
- .join('\n')
- .trim();
-
- return {
- type: 'quote',
- content: match[0],
- quote: quote,
- } as TextElementQuote;
-}
diff --git a/src/mfm/parse/elements/search.ts b/src/mfm/parse/elements/search.ts
deleted file mode 100644
index f51844b079..0000000000
--- a/src/mfm/parse/elements/search.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Search
- */
-
-export type TextElementSearch = {
- type: 'search';
- content: string;
- query: string;
-};
-
-export default function(text: string) {
- const match = text.match(/^(.+?)( | )(検索|\[検索\]|Search|\[Search\])(\n|$)/i);
- if (!match) return null;
- return {
- type: 'search',
- content: match[0],
- query: match[1]
- };
-}
diff --git a/src/mfm/parse/elements/title.ts b/src/mfm/parse/elements/title.ts
deleted file mode 100644
index a9922c8aca..0000000000
--- a/src/mfm/parse/elements/title.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Title
- */
-
-export type TextElementTitle = {
- type: 'title';
- content: string;
- title: string;
-};
-
-export default function(text: string, before: string) {
- const isBegin = before == '';
-
- const match = isBegin ? text.match(/^(【|\[)(.+?)(】|])\n/) : text.match(/^\n(【|\[)(.+?)(】|])\n/);
- if (!match) return null;
- return {
- type: 'title',
- content: match[0],
- title: match[2]
- } as TextElementTitle;
-}
diff --git a/src/mfm/parse/elements/url.ts b/src/mfm/parse/elements/url.ts
deleted file mode 100644
index a16f67f2c2..0000000000
--- a/src/mfm/parse/elements/url.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * URL
- */
-
-export type TextElementUrl = {
- type: 'url';
- content: string;
- url: string;
-};
-
-export default function(text: string, before: string) {
- const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/);
- if (!match) return null;
- let url = match[0];
- if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.'));
- if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(','));
- if (url.endsWith(')') && before.endsWith('(')) url = url.substr(0, url.lastIndexOf(')'));
- return {
- type: 'url',
- content: url,
- url: url
- } as TextElementUrl;
-}
diff --git a/src/mfm/parse/index.ts b/src/mfm/parse/index.ts
deleted file mode 100644
index 7697bb6e36..0000000000
--- a/src/mfm/parse/index.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * Misskey Text Analyzer
- */
-
-import { TextElementBold } from './elements/bold';
-import { TextElementBig } from './elements/big';
-import { TextElementCode } from './elements/code';
-import { TextElementEmoji } from './elements/emoji';
-import { TextElementHashtag } from './elements/hashtag';
-import { TextElementInlineCode } from './elements/inline-code';
-import { TextElementMath } from './elements/math';
-import { TextElementLink } from './elements/link';
-import { TextElementMention } from './elements/mention';
-import { TextElementQuote } from './elements/quote';
-import { TextElementSearch } from './elements/search';
-import { TextElementTitle } from './elements/title';
-import { TextElementUrl } from './elements/url';
-import { TextElementMotion } from './elements/motion';
-import { groupOn } from '../../prelude/array';
-import * as A from '../../prelude/array';
-import * as S from '../../prelude/string';
-
-const elements = [
- require('./elements/big'),
- require('./elements/bold'),
- require('./elements/title'),
- require('./elements/url'),
- require('./elements/link'),
- require('./elements/mention'),
- require('./elements/hashtag'),
- require('./elements/code'),
- require('./elements/inline-code'),
- require('./elements/math'),
- require('./elements/quote'),
- require('./elements/emoji'),
- require('./elements/search'),
- require('./elements/motion')
-].map(element => element.default as TextElementProcessor);
-
-export type TextElement = { type: 'text', content: string }
- | TextElementBold
- | TextElementBig
- | TextElementCode
- | TextElementEmoji
- | TextElementHashtag
- | TextElementInlineCode
- | TextElementMath
- | TextElementLink
- | TextElementMention
- | TextElementQuote
- | TextElementSearch
- | TextElementTitle
- | TextElementUrl
- | TextElementMotion;
-export type TextElementProcessor = (text: string, before: string) => TextElement | TextElement[];
-
-export default (source: string): TextElement[] => {
- if (source == null || source == '') {
- return null;
- }
-
- const tokens: TextElement[] = [];
-
- function push(token: TextElement) {
- if (token != null) {
- tokens.push(token);
- source = source.substr(token.content.length);
- }
- }
-
- // パース
- while (source != '') {
- const parsed = elements.some(el => {
- let _tokens = el(source, tokens.map(token => token.content).join(''));
- if (_tokens) {
- if (!Array.isArray(_tokens)) {
- _tokens = [_tokens];
- }
- _tokens.forEach(push);
- return true;
- } else {
- return false;
- }
- });
-
- if (!parsed) {
- push({
- type: 'text',
- content: source[0]
- });
- }
- }
-
- const combineText = (es: TextElement[]): TextElement =>
- ({ type: 'text', content: S.concat(es.map(e => e.content)) });
-
- return A.concat(groupOn(x => x.type, tokens).map(es =>
- es[0].type === 'text' ? [combineText(es)] : es
- ));
-};
diff --git a/src/mfm/parser.ts b/src/mfm/parser.ts
new file mode 100644
index 0000000000..5f89696c20
--- /dev/null
+++ b/src/mfm/parser.ts
@@ -0,0 +1,256 @@
+import * as P from 'parsimmon';
+import parseAcct from '../misc/acct/parse';
+import { toUnicode } from 'punycode';
+
+const emojiRegex = /((?:\ud83d[\udc68\udc69])(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddb0-\uddb3])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f)|[\u0023\u002a\u0030-\u0039]\ufe0f?\u20e3|(?:[\u00a9\u00ae\u2122\u265f]\ufe0f)|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37\udf21\udf24-\udf2c\udf36\udf7d\udf96\udf97\udf99-\udf9b\udf9e\udf9f\udfcd\udfce\udfd4-\udfdf\udff3\udff5\udff7]|\ud83d[\udc3f\udc41\udcfd\udd49\udd4a\udd6f\udd70\udd73\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa\udecb\udecd-\udecf\udee0-\udee5\udee9\udef0\udef3]|[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c\u26a0\u26a1\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e))|(?:(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75\udd90]|[\u261d\u26f7\u26f9\u270c\u270d])(?:\ufe0f|(?!\ufe0e))|(?:\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd7a\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0\udecc]|\ud83e[\udd18-\udd1c\udd1e\udd1f\udd26\udd30-\udd39\udd3d\udd3e\uddb5\uddb6\uddb8\uddb9\uddd1-\udddd]|[\u270a\u270b]))(?:\ud83c[\udffb-\udfff])?|(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf3\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf20\udf2d-\udf35\udf37-\udf7c\udf7e-\udf84\udf86-\udf93\udfa0-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcf-\udfd3\udfe0-\udff0\udff4\udff8-\udfff]|\ud83d[\udc00-\udc3e\udc40\udc44\udc45\udc51-\udc65\udc6a-\udc6d\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udca9\udcab-\udcfc\udcff-\udd3d\udd4b-\udd4e\udd50-\udd67\udda4\uddfb-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\uded0-\uded2\udeeb\udeec\udef4-\udef9]|\ud83e[\udd10-\udd17\udd1d\udd20-\udd25\udd27-\udd2f\udd3a\udd3c\udd40-\udd45\udd47-\udd70\udd73-\udd76\udd7a\udd7c-\udda2\uddb4\uddb7\uddc0-\uddc2\uddd0\uddde-\uddff]|[\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797\u27b0\u27bf\ue50a])|\ufe0f)/;
+
+export type Node = {
+ name: string,
+ children?: Node[],
+ props?: any;
+};
+
+function _makeNode(name: string, children?: Node[], props?: any): Node {
+ return children ? {
+ name,
+ children,
+ props
+ } : {
+ name,
+ props
+ };
+}
+
+function makeNode(name: string, props?: any): Node {
+ return _makeNode(name, null, props);
+}
+
+function makeNodeWithChildren(name: string, children: Node[], props?: any): Node {
+ return _makeNode(name, children, props);
+}
+
+const newline = P((input, i) => {
+ if (i == 0 || input[i] == '\n' || input[i - 1] == '\n') {
+ return P.makeSuccess(i, null);
+ } else {
+ return P.makeFailure(i, 'not newline');
+ }
+});
+
+const mfm = P.createLanguage({
+ root: r => P.alt(
+ r.big,
+ r.bold,
+ r.motion,
+ r.url,
+ r.link,
+ r.mention,
+ r.hashtag,
+ r.emoji,
+ r.blockCode,
+ r.inlineCode,
+ r.quote,
+ r.math,
+ r.search,
+ r.title,
+ r.text
+ ).atLeast(1),
+
+ text: () => P.any.map(x => makeNode('text', { text: x })),
+
+ //#region Big
+ big: r =>
+ P.regexp(/^\*\*\*([\s\S]+?)\*\*\*/, 1)
+ .map(x => makeNodeWithChildren('big', P.alt(
+ r.mention,
+ r.emoji,
+ r.text
+ ).atLeast(1).tryParse(x))),
+ //#endregion
+
+ //#region Block code
+ blockCode: r =>
+ newline.then(
+ P((input, i) => {
+ const text = input.substr(i);
+ const match = text.match(/^```(.+?)?\n([\s\S]+?)\n```(\n|$)/i);
+ if (!match) return P.makeFailure(i, 'not a blockCode');
+ return P.makeSuccess(i + match[0].length, makeNode('blockCode', { code: match[2], lang: match[1] ? match[1].trim() : null }));
+ })
+ ),
+ //#endregion
+
+ //#region Bold
+ bold: r =>
+ P.regexp(/\*\*([\s\S]+?)\*\*/, 1)
+ .map(x => makeNodeWithChildren('bold', P.alt(
+ r.mention,
+ r.emoji,
+ r.text
+ ).atLeast(1).tryParse(x))),
+ //#endregion
+
+ //#region Emoji
+ emoji: r =>
+ P.alt(
+ P.regexp(/:([a-z0-9_+-]+):/i, 1)
+ .map(x => makeNode('emoji', {
+ name: x
+ })),
+ P.regexp(emojiRegex)
+ .map(x => makeNode('emoji', {
+ emoji: x
+ })),
+ ),
+ //#endregion
+
+ //#region Hashtag
+ hashtag: r =>
+ P((input, i) => {
+ const text = input.substr(i);
+ const match = text.match(/^#([^\s\.,!\?#]+)/i);
+ if (!match) return P.makeFailure(i, 'not a hashtag');
+ if (input[i - 1] != ' ' && input[i - 1] != null) return P.makeFailure(i, 'require space before "#"');
+ return P.makeSuccess(i + match[0].length, makeNode('hashtag', { hashtag: match[1] }));
+ }),
+ //#endregion
+
+ //#region Inline code
+ inlineCode: r =>
+ P.regexp(/`(.+?)`/, 1)
+ .map(x => makeNode('inlineCode', { code: x })),
+ //#endregion
+
+ //#region Link
+ link: r =>
+ P.seqObj(
+ ['silent', P.string('?').fallback(null).map(x => x != null)] as any,
+ P.string('['),
+ ['text', P.regexp(/[^\n\[\]]+/)] as any,
+ P.string(']'),
+ P.string('('),
+ ['url', r.url] as any,
+ P.string(')'),
+ )
+ .map((x: any) => {
+ return makeNodeWithChildren('link', P.alt(
+ r.big,
+ r.bold,
+ r.motion,
+ r.emoji,
+ r.text
+ ).atLeast(1).tryParse(x.text), {
+ silent: x.silent,
+ url: x.url.props.url
+ });
+ }),
+ //#endregion
+
+ //#region Math
+ math: r =>
+ P.regexp(/\\\((.+?)\\\)/, 1)
+ .map(x => makeNode('math', { formula: x })),
+ //#endregion
+
+ //#region Mention
+ mention: r =>
+ P((input, i) => {
+ const text = input.substr(i);
+ const match = text.match(/^@[a-z0-9_]+(?:@[a-z0-9\.\-]+[a-z0-9])?/i);
+ if (!match) return P.makeFailure(i, 'not a mention');
+ if (input[i - 1] != null && input[i - 1].match(/[a-z0-9]/i)) return P.makeFailure(i, 'not a mention');
+ return P.makeSuccess(i + match[0].length, match[0]);
+ })
+ .map(x => {
+ const { username, host } = parseAcct(x.substr(1));
+ const canonical = host != null ? `@${username}@${toUnicode(host)}` : x;
+ return makeNode('mention', {
+ canonical, username, host, acct: x
+ });
+ }),
+ //#endregion
+
+ //#region Motion
+ motion: r =>
+ P.alt(P.regexp(/\(\(\(([\s\S]+?)\)\)\)/, 1), P.regexp(/<motion>(.+?)<\/motion>/, 1))
+ .map(x => makeNodeWithChildren('motion', P.alt(
+ r.bold,
+ r.mention,
+ r.emoji,
+ r.text
+ ).atLeast(1).tryParse(x))),
+ //#endregion
+
+ //#region Quote
+ quote: r =>
+ newline.then(P((input, i) => {
+ const text = input.substr(i);
+ if (!text.match(/^>[\s\S]+?/)) return P.makeFailure(i, 'not a quote');
+ const quote: string[] = [];
+ text.split('\n').some(line => {
+ if (line.startsWith('>')) {
+ quote.push(line);
+ return false;
+ } else {
+ return true;
+ }
+ });
+ const qInner = quote.join('\n').replace(/^>/gm, '').replace(/^ /gm, '');
+ if (qInner == '') return P.makeFailure(i, 'not a quote');
+ const contents = r.root.tryParse(qInner);
+ return P.makeSuccess(i + quote.join('\n').length, makeNodeWithChildren('quote', contents));
+ })),
+ //#endregion
+
+ //#region Search
+ search: r =>
+ newline.then(P((input, i) => {
+ const text = input.substr(i);
+ const match = text.match(/^(.+?)( | )(検索|\[検索\]|Search|\[Search\])(\n|$)/i);
+ if (!match) return P.makeFailure(i, 'not a search');
+ return P.makeSuccess(i + match[0].length, makeNode('search', { query: match[1], content: match[0].trim() }));
+ })),
+ //#endregion
+
+ //#region Title
+ title: r =>
+ newline.then(P((input, i) => {
+ const text = input.substr(i);
+ const match = text.match(/^((【|\[)(.+?)(】|]))(\n|$)/);
+ if (!match) return P.makeFailure(i, 'not a title');
+ const q = match[1].trim().substring(1, match[1].length - 1);
+ const contents = P.alt(
+ r.big,
+ r.bold,
+ r.motion,
+ r.url,
+ r.link,
+ r.mention,
+ r.hashtag,
+ r.emoji,
+ r.inlineCode,
+ r.text
+ ).atLeast(1).tryParse(q);
+ return P.makeSuccess(i + match[0].length, makeNodeWithChildren('title', contents));
+ })),
+ //#endregion
+
+ //#region URL
+ url: r =>
+ P((input, i) => {
+ const text = input.substr(i);
+ const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/);
+ if (!match) return P.makeFailure(i, 'not a url');
+ let url = match[0];
+ const before = input[i - 1];
+ if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.'));
+ if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(','));
+ if (url.endsWith(')') && before == '(') url = url.substr(0, url.lastIndexOf(')'));
+ if (url.endsWith(']') && before == '[') url = url.substr(0, url.lastIndexOf(']'));
+ return P.makeSuccess(i + url.length, url);
+ })
+ .map(x => makeNode('url', { url: x })),
+ //#endregion
+});
+
+export default mfm;
diff --git a/src/mfm/parse/core/syntax-highlighter.ts b/src/mfm/syntax-highlight.ts
index 83aac89f1b..3a2b90588b 100644
--- a/src/mfm/parse/core/syntax-highlighter.ts
+++ b/src/mfm/syntax-highlight.ts
@@ -1,4 +1,4 @@
-import { capitalize, toUpperCase } from "../../../prelude/string";
+import { capitalize, toUpperCase } from "../prelude/string";
function escape(text: string) {
return text
@@ -308,7 +308,7 @@ const elements: Element[] = [
];
// specify lang is todo
-export default (source: string, lang?: string) => {
+export default (source: string, lang?: string): string => {
let code = source;
let html = '';