diff --git a/src/api.ts b/src/api.ts
new file mode 100644
index 00000000..88e17e8b
--- /dev/null
+++ b/src/api.ts
@@ -0,0 +1,58 @@
+import peg from 'pegjs';
+import { MfmNode, MfmPlainNode } from './node';
+import { stringifyNode, stringifyTree } from './util';
+
+const parser: peg.Parser = require('./parser');
+
+export function parse(input: string): MfmNode[] {
+ const nodes = parser.parse(input, { startRule: 'fullParser' });
+ return nodes;
+}
+
+export function parsePlain(input: string): MfmPlainNode[] {
+ const nodes = parser.parse(input, { startRule: 'plainParser' });
+ return nodes;
+}
+
+export function toString(tree: MfmNode[]): string
+export function toString(node: MfmNode): string
+export function toString(node: MfmNode | MfmNode[]): string {
+ if (Array.isArray(node)) {
+ return stringifyTree(node);
+ }
+ else {
+ return stringifyNode(node);
+ }
+}
+
+export function inspect(tree: MfmNode[], action: (node: MfmNode) => void): void {
+ for (const node of tree) {
+ action(node);
+ if (node.children != null) {
+ inspect(node.children, action);
+ }
+ }
+}
+
+export function extract(nodes: MfmNode[], type: (MfmNode['type'] | MfmNode['type'][])): MfmNode[] {
+ function predicate(node: MfmNode, type: (MfmNode['type'] | MfmNode['type'][])): boolean {
+ if (Array.isArray(type)) {
+ return (type.some(i => i == node.type));
+ }
+ else {
+ return (type == node.type);
+ }
+ }
+
+ const dest = [] as MfmNode[];
+ for (const node of nodes) {
+ if (predicate(node, type)) {
+ dest.push(node);
+ }
+ if (node.children != null) {
+ dest.push(...extract(node.children, type));
+ }
+ }
+
+ return dest;
+}
diff --git a/src/index.ts b/src/index.ts
index 7c47787c..292d5ace 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,60 +1,10 @@
-import peg from 'pegjs';
-import { MfmNode, MfmPlainNode } from './node';
-import { stringifyNode, stringifyTree } from './util';
-const parser: peg.Parser = require('./parser');
-
-export function parse(input: string): MfmNode[] {
- const nodes = parser.parse(input, { startRule: 'fullParser' });
- return nodes;
-}
-
-export function parsePlain(input: string): MfmPlainNode[] {
- const nodes = parser.parse(input, { startRule: 'plainParser' });
- return nodes;
-}
-
-export function toString(tree: MfmNode[]): string
-export function toString(node: MfmNode): string
-export function toString(node: MfmNode | MfmNode[]): string {
- if (Array.isArray(node)) {
- return stringifyTree(node);
- }
- else {
- return stringifyNode(node);
- }
-}
-
-export function inspect(tree: MfmNode[], action: (node: MfmNode) => void): void {
- for (const node of tree) {
- action(node);
- if (node.children != null) {
- inspect(node.children, action);
- }
- }
-}
-
-export function extract(nodes: MfmNode[], type: (MfmNode['type'] | MfmNode['type'][])): MfmNode[] {
- function predicate(node: MfmNode, type: (MfmNode['type'] | MfmNode['type'][])): boolean {
- if (Array.isArray(type)) {
- return (type.some(i => i == node.type));
- }
- else {
- return (type == node.type);
- }
- }
-
- const dest = [] as MfmNode[];
- for (const node of nodes) {
- if (predicate(node, type)) {
- dest.push(node);
- }
- if (node.children != null) {
- dest.push(...extract(node.children, type));
- }
- }
-
- return dest;
-}
+export {
+ parse,
+ parsePlain,
+ toString,
+ inspect,
+ extract
+} from './api';
export { NodeType } from './node';
diff --git a/test/api.ts b/test/api.ts
new file mode 100644
index 00000000..910cd3cd
--- /dev/null
+++ b/test/api.ts
@@ -0,0 +1,58 @@
+import assert from 'assert';
+import { extract, inspect, parse, toString } from '../built/index';
+import {
+ TEXT, CENTER, FN, UNI_EMOJI, MENTION, EMOJI_CODE, HASHTAG, N_URL, BOLD, SMALL, ITALIC, STRIKE, QUOTE, MATH_BLOCK, SEARCH, CODE_BLOCK, LINK
+} from './node';
+
+describe('API', () => {
+ describe('toString', () => {
+ it('basic', () => {
+ const input =
+`before
+
+Hello [tada everynyan! 🎉]
+
+I'm @ai, A bot of misskey!
+
+https://github.com/syuilo/ai
+
+after`;
+ assert.strictEqual(toString(parse(input)), input);
+ });
+ });
+
+ describe('inspect', () => {
+ it('replace text', () => {
+ const input = 'good morning [tada everynyan!]';
+ const result = parse(input);
+ inspect(result, node => {
+ if (node.type == 'text') {
+ node.props.text = node.props.text.replace(/good morning/g, 'hello');
+ }
+ });
+ assert.strictEqual(toString(result), 'hello [tada everynyan!]');
+ });
+ });
+
+ describe('extract', () => {
+ it('basic', () => {
+ const nodes = parse('@hoge @piyo @bebeyo');
+ const expect = [
+ MENTION('hoge', null, '@hoge'),
+ MENTION('piyo', null, '@piyo'),
+ MENTION('bebeyo', null, '@bebeyo')
+ ];
+ assert.deepStrictEqual(extract(nodes, 'mention'), expect);
+ });
+
+ it('nested', () => {
+ const nodes = parse('abc:hoge:[tada 123 @hoge :foo:]:piyo:');
+ const expect = [
+ EMOJI_CODE('hoge'),
+ EMOJI_CODE('foo'),
+ EMOJI_CODE('piyo')
+ ];
+ assert.deepStrictEqual(extract(nodes, 'emojiCode'), expect);
+ });
+ });
+});
diff --git a/test/main.ts b/test/main.ts
deleted file mode 100644
index 7ec63038..00000000
--- a/test/main.ts
+++ /dev/null
@@ -1,564 +0,0 @@
-import assert from 'assert';
-import { extract, inspect, parse, parsePlain, toString } from '../built/index';
-import { createNode } from '../built/util';
-import {
- TEXT, CENTER, FN, UNI_EMOJI, MENTION, EMOJI_CODE, HASHTAG, N_URL, BOLD, SMALL, ITALIC, STRIKE, QUOTE, MATH_BLOCK, SEARCH, CODE_BLOCK, LINK
-} from './node';
-
-describe('text', () => {
- it('basic', () => {
- const input = 'abc';
- const output = [TEXT('abc')];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('quote', () => {
- it('single', () => {
- const input = '> abc';
- const output = [
- QUOTE([
- TEXT('abc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('multiple', () => {
- const input = `
-> abc
-> 123
-`;
- const output = [
- QUOTE([
- TEXT('abc\n123')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-
- it('with block (center)', () => {
- const input = `
->
-> a
->
-`;
- const output = [
- QUOTE([
- CENTER([
- TEXT('a')
- ])
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-
- it('with block (center, mention)', () => {
- const input = `
->
-> I'm @ai, An bot of misskey!
->
-`;
- const output = [
- QUOTE([
- CENTER([
- TEXT('I\'m '),
- MENTION('ai', null, '@ai'),
- TEXT(', An bot of misskey!'),
- ])
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('search', () => {
- describe('basic', () => {
- it('Search', () => {
- const input = 'MFM 書き方 123 Search';
- const output = [
- createNode('search', {
- query: 'MFM 書き方 123',
- content: input
- })
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('[Search]', () => {
- const input = 'MFM 書き方 123 [Search]';
- const output = [
- createNode('search', {
- query: 'MFM 書き方 123',
- content: input
- })
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('search', () => {
- const input = 'MFM 書き方 123 search';
- const output = [
- createNode('search', {
- query: 'MFM 書き方 123',
- content: input
- })
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('[search]', () => {
- const input = 'MFM 書き方 123 [search]';
- const output = [
- createNode('search', {
- query: 'MFM 書き方 123',
- content: input
- })
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('検索', () => {
- const input = 'MFM 書き方 123 検索';
- const output = [
- createNode('search', {
- query: 'MFM 書き方 123',
- content: input
- })
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('[検索]', () => {
- const input = 'MFM 書き方 123 [検索]';
- const output = [
- createNode('search', {
- query: 'MFM 書き方 123',
- content: input
- })
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- });
- it('with text', () => {
- const input = 'abc\nhoge piyo bebeyo 検索\n123';
- const output = [
- TEXT('abc'),
- SEARCH('hoge piyo bebeyo', 'hoge piyo bebeyo 検索'),
- TEXT('123')
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('code block', () => {
- it('basic', () => {
- const input = '```\nabc\n```';
- const output = [CODE_BLOCK('abc', null)];
- assert.deepStrictEqual(parse(input), output);
- });
- it('multi line', () => {
- const input = '```\na\nb\nc\n```';
- const output = [CODE_BLOCK('a\nb\nc', null)];
- assert.deepStrictEqual(parse(input), output);
- });
- it('basic (lang)', () => {
- const input = '```js\nconst a = 1;\n```';
- const output = [CODE_BLOCK('const a = 1;', 'js')];
- assert.deepStrictEqual(parse(input), output);
- });
- it('with text', () => {
- const input = 'abc\n```\nconst abc = 1;\n```\n123';
- const output = [
- TEXT('abc'),
- CODE_BLOCK('const abc = 1;', null),
- TEXT('123')
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('mathBlock', () => {
- it('basic', () => {
- const input = '123\n\\[math1\\]\nabc\n\\[math2\\]';
- const output = [
- TEXT('123'),
- MATH_BLOCK('math1'),
- TEXT('abc'),
- MATH_BLOCK('math2')
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('case of no matched', () => {
- const input = '\\[aaa\\]\\[bbb\\]';
- const output = [
- TEXT('\\[aaa\\]\\[bbb\\]')
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('center', () => {
- it('single text', () => {
- const input = 'abc';
- const output = [
- CENTER([
- TEXT('abc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('multiple text', () => {
- const input = 'before\n\nabc\n123\n\npiyo\n\nafter';
- const output = [
- TEXT('before'),
- CENTER([
- TEXT('abc\n123\n\npiyo')
- ]),
- TEXT('after')
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('emoji code', () => {
- it('basic', () => {
- const input = ':abc:';
- const output = [EMOJI_CODE('abc')];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('unicode emoji', () => {
- it('basic', () => {
- const input = '今起きた😇';
- const output = [TEXT('今起きた'), UNI_EMOJI('😇')];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('big', () => {
- it('basic', () => {
- const input = '***abc***';
- const output = [
- FN('tada', { }, [
- TEXT('abc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('内容にはインライン構文を利用できる', () => {
- const input = '***123**abc**123***';
- const output = [
- FN('tada', { }, [
- TEXT('123'),
- BOLD([
- TEXT('abc')
- ]),
- TEXT('123')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('内容は改行できる', () => {
- const input = '***123\n**abc**\n123***';
- const output = [
- FN('tada', { }, [
- TEXT('123\n'),
- BOLD([
- TEXT('abc')
- ]),
- TEXT('\n123')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('bold', () => {
- it('basic', () => {
- const input = '**abc**';
- const output = [
- BOLD([
- TEXT('abc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('内容にはインライン構文を利用できる', () => {
- const input = '**123~~abc~~123**';
- const output = [
- BOLD([
- TEXT('123'),
- STRIKE([
- TEXT('abc')
- ]),
- TEXT('123')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('内容は改行できる', () => {
- const input = '**123\n~~abc~~\n123**';
- const output = [
- BOLD([
- TEXT('123\n'),
- STRIKE([
- TEXT('abc')
- ]),
- TEXT('\n123')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('small', () => {
- it('basic', () => {
- const input = 'abc';
- const output = [
- SMALL([
- TEXT('abc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('内容にはインライン構文を利用できる', () => {
- const input = 'abc**123**abc';
- const output = [
- SMALL([
- TEXT('abc'),
- BOLD([
- TEXT('123')
- ]),
- TEXT('abc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('内容は改行できる', () => {
- const input = 'abc\n**123**\nabc';
- const output = [
- SMALL([
- TEXT('abc\n'),
- BOLD([
- TEXT('123')
- ]),
- TEXT('\nabc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('italic 1', () => {
- it('basic', () => {
- const input = 'abc';
- const output = [
- ITALIC([
- TEXT('abc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('内容にはインライン構文を利用できる', () => {
- const input = 'abc**123**abc';
- const output = [
- ITALIC([
- TEXT('abc'),
- BOLD([
- TEXT('123')
- ]),
- TEXT('abc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
- it('内容は改行できる', () => {
- const input = 'abc\n**123**\nabc';
- const output = [
- ITALIC([
- TEXT('abc\n'),
- BOLD([
- TEXT('123')
- ]),
- TEXT('\nabc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('italic 2', () => {
- it('basic', () => {
- const input = '*abc*';
- const output = [
- ITALIC([
- TEXT('abc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-// strike
-
-// inlineCode
-
-// mathInline
-
-// mention
-
-describe('hashtag', () => {
- it('and unicode emoji', () => {
- const input = '#️⃣abc123#abc';
- const output = [UNI_EMOJI('#️⃣'), TEXT('abc123'), HASHTAG('abc')];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('url', () => {
- it('basic', () => {
- const input = 'official instance: https://misskey.io/@ai.';
- const output = [
- TEXT('official instance: '),
- N_URL('https://misskey.io/@ai'),
- TEXT('.')
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('link', () => {
- it('basic', () => {
- const input = '[official instance](https://misskey.io/@ai).';
- const output = [
- LINK(false, 'https://misskey.io/@ai', [
- TEXT('official instance')
- ]),
- TEXT('.')
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-
- it('silent flag', () => {
- const input = '?[official instance](https://misskey.io/@ai).';
- const output = [
- LINK(true, 'https://misskey.io/@ai', [
- TEXT('official instance')
- ]),
- TEXT('.')
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-
- it('do not yield url node even if label is recognisable as a url', () => {
- const input = 'official instance: [https://misskey.io/@ai](https://misskey.io/@ai).';
- const output = [
- TEXT('official instance: '),
- LINK(false, 'https://misskey.io/@ai', [
- TEXT('https://misskey.io/@ai')
- ]),
- TEXT('.')
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-
- it('do not yield link node even if label is recognisable as a link', () => {
- const input = 'official instance: [[https://misskey.io/@ai](https://misskey.io/@ai)](https://misskey.io/@ai).';
- const output = [
- TEXT('official instance: '),
- LINK(false, 'https://misskey.io/@ai', [
- TEXT('[https://misskey.io/@ai](https://misskey.io/@ai)')
- ]),
- TEXT('.')
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-describe('fn', () => {
- it('basic', () => {
- const input = '[tada abc]';
- const output = [
- FN('tada', { }, [
- TEXT('abc')
- ])
- ];
- assert.deepStrictEqual(parse(input), output);
- });
-});
-
-it('composite', () => {
- const input =
-`before
-
-Hello [tada everynyan! 🎉]
-
-I'm @ai, A bot of misskey!
-
-https://github.com/syuilo/ai
-
-after`;
- const output = [
- TEXT('before'),
- CENTER([
- TEXT('Hello '),
- FN('tada', { }, [
- TEXT('everynyan! '),
- UNI_EMOJI('🎉')
- ]),
- TEXT('\n\nI\'m '),
- MENTION('ai', null, '@ai'),
- TEXT(', A bot of misskey!\n\n'),
- N_URL('https://github.com/syuilo/ai')
- ]),
- TEXT('after')
- ];
- assert.deepStrictEqual(parse(input), output);
-});
-
-describe('toString API', () => {
- it('basic', () => {
- const input =
-`before
-
-Hello [tada everynyan! 🎉]
-
-I'm @ai, A bot of misskey!
-
-https://github.com/syuilo/ai
-
-after`;
- assert.strictEqual(toString(parse(input)), input);
- });
-});
-
-describe('inspect API', () => {
- it('replace text', () => {
- const input = 'good morning [tada everynyan!]';
- const result = parse(input);
- inspect(result, node => {
- if (node.type == 'text') {
- node.props.text = node.props.text.replace(/good morning/g, 'hello');
- }
- });
- assert.strictEqual(toString(result), 'hello [tada everynyan!]');
- });
-});
-
-describe('extract API', () => {
- it('basic', () => {
- const nodes = parse('@hoge @piyo @bebeyo');
- const expect = [
- MENTION('hoge', null, '@hoge'),
- MENTION('piyo', null, '@piyo'),
- MENTION('bebeyo', null, '@bebeyo')
- ];
- assert.deepStrictEqual(extract(nodes, 'mention'), expect);
- });
-
- it('nested', () => {
- const nodes = parse('abc:hoge:[tada 123 @hoge :foo:]:piyo:');
- const expect = [
- EMOJI_CODE('hoge'),
- EMOJI_CODE('foo'),
- EMOJI_CODE('piyo')
- ];
- assert.deepStrictEqual(extract(nodes, 'emojiCode'), expect);
- });
-});
diff --git a/test/node.ts b/test/node.ts
index 7116e9a4..8f9918d5 100644
--- a/test/node.ts
+++ b/test/node.ts
@@ -1,26 +1,24 @@
import {
- MfmBold, MfmCenter, MfmCodeBlock, MfmEmojiCode, MfmFn, MfmHashtag, MfmInline,
- MfmInlineCode, MfmItalic, MfmLink, MfmMathBlock, MfmMathInline, MfmMention,
- MfmNode, MfmQuote, MfmSearch, MfmSmall, MfmStrike, MfmText, MfmUnicodeEmoji, MfmUrl
+ MfmFn, MfmInline, MfmNode, NodeType
} from '../built';
+
+export const QUOTE = (children: MfmNode[]): NodeType<'quote'> => { return { type:'quote', children }; };
+export const SEARCH = (query: string, content: string): NodeType<'search'> => { return { type:'search', props: { query, content } }; };
+export const CODE_BLOCK = (code: string, lang: string | null): NodeType<'blockCode'> => { return { type:'blockCode', props: { code, lang } }; };
+export const MATH_BLOCK = (formula: string): NodeType<'mathBlock'> => { return { type:'mathBlock', props: { formula } }; };
+export const CENTER = (children: MfmInline[]): NodeType<'center'> => { return { type:'center', children }; };
-export const QUOTE = (children: MfmNode[]): MfmQuote => { return { type:'quote', children }; };
-export const SEARCH = (query: string, content: string): MfmSearch => { return { type:'search', props: { query, content } }; };
-export const CODE_BLOCK = (code: string, lang: string | null): MfmCodeBlock => { return { type:'blockCode', props: { code, lang } }; };
-export const MATH_BLOCK = (formula: string): MfmMathBlock => { return { type:'mathBlock', props: { formula } }; };
-export const CENTER = (children: MfmInline[]): MfmCenter => { return { type:'center', children }; };
-
-export const BOLD = (children: MfmInline[]): MfmBold => { return { type:'bold', children }; };
-export const SMALL = (children: MfmInline[]): MfmSmall => { return { type:'small', children }; };
-export const ITALIC = (children: MfmInline[]): MfmItalic => { return { type:'italic', children }; };
-export const STRIKE = (children: MfmInline[]): MfmStrike => { return { type:'strike', children }; };
-export const INLINE_CODE = (code: string): MfmInlineCode => { return { type:'inlineCode', props: { code } }; };
-export const MATH_INLINE = (formula: string): MfmMathInline => { return { type:'mathInline', props: { formula } }; };
-export const MENTION = (username: string, host: string | null, acct: string): MfmMention => { return { type:'mention', props: { username, host, acct } }; };
-export const HASHTAG = (value: string): MfmHashtag => { return { type:'hashtag', props: { hashtag: value } }; };
-export const N_URL = (value: string): MfmUrl => { return { type:'url', props: { url: value } }; };
-export const LINK = (silent: boolean, url: string, children: MfmInline[]): MfmLink => { return { type:'link', props: { silent, url }, children }; };
-export const EMOJI_CODE = (name: string): MfmEmojiCode => { return { type:'emojiCode', props: { name: name } }; };
-export const FN = (name: string, args: MfmFn['props']['args'], children: MfmFn['children']): MfmFn => { return { type:'fn', props: { name, args }, children }; };
-export const UNI_EMOJI = (value: string): MfmUnicodeEmoji => { return { type:'unicodeEmoji', props: { emoji: value } }; };
-export const TEXT = (value: string): MfmText => { return { type:'text', props: { text: value } }; };
+export const BOLD = (children: MfmInline[]): NodeType<'bold'> => { return { type:'bold', children }; };
+export const SMALL = (children: MfmInline[]): NodeType<'small'> => { return { type:'small', children }; };
+export const ITALIC = (children: MfmInline[]): NodeType<'italic'> => { return { type:'italic', children }; };
+export const STRIKE = (children: MfmInline[]): NodeType<'strike'> => { return { type:'strike', children }; };
+export const INLINE_CODE = (code: string): NodeType<'inlineCode'> => { return { type:'inlineCode', props: { code } }; };
+export const MATH_INLINE = (formula: string): NodeType<'mathInline'> => { return { type:'mathInline', props: { formula } }; };
+export const MENTION = (username: string, host: string | null, acct: string): NodeType<'mention'> => { return { type:'mention', props: { username, host, acct } }; };
+export const HASHTAG = (value: string): NodeType<'hashtag'> => { return { type:'hashtag', props: { hashtag: value } }; };
+export const N_URL = (value: string): NodeType<'url'> => { return { type:'url', props: { url: value } }; };
+export const LINK = (silent: boolean, url: string, children: MfmInline[]): NodeType<'link'> => { return { type:'link', props: { silent, url }, children }; };
+export const EMOJI_CODE = (name: string): NodeType<'emojiCode'> => { return { type:'emojiCode', props: { name: name } }; };
+export const FN = (name: string, args: MfmFn['props']['args'], children: MfmFn['children']): NodeType<'fn'> => { return { type:'fn', props: { name, args }, children }; };
+export const UNI_EMOJI = (value: string): NodeType<'unicodeEmoji'> => { return { type:'unicodeEmoji', props: { emoji: value } }; };
+export const TEXT = (value: string): NodeType<'text'> => { return { type:'text', props: { text: value } }; };
diff --git a/test/parser.ts b/test/parser.ts
new file mode 100644
index 00000000..45cf536c
--- /dev/null
+++ b/test/parser.ts
@@ -0,0 +1,526 @@
+import assert from 'assert';
+import { parse } from '../built/index';
+import { createNode } from '../built/util';
+import {
+ TEXT, CENTER, FN, UNI_EMOJI, MENTION, EMOJI_CODE, HASHTAG, N_URL, BOLD, SMALL, ITALIC, STRIKE, QUOTE, MATH_BLOCK, SEARCH, CODE_BLOCK, LINK
+} from './node';
+
+describe('parser', () => {
+ describe('text', () => {
+ it('普通のテキストを入力すると1つのテキストノードが返される', () => {
+ const input = 'abc';
+ const output = [TEXT('abc')];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('quote', () => {
+ it('1行の引用ブロックを使用できる', () => {
+ const input = '> abc';
+ const output = [
+ QUOTE([
+ TEXT('abc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('複数行の引用ブロックを使用できる', () => {
+ const input = `
+> abc
+> 123
+`;
+ const output = [
+ QUOTE([
+ TEXT('abc\n123')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('引用ブロックはブロックをネストできる', () => {
+ const input = `
+>
+> a
+>
+`;
+ const output = [
+ QUOTE([
+ CENTER([
+ TEXT('a')
+ ])
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('引用ブロックはインライン構文を含んだブロックをネストできる', () => {
+ const input = `
+>
+> I'm @ai, An bot of misskey!
+>
+`;
+ const output = [
+ QUOTE([
+ CENTER([
+ TEXT('I\'m '),
+ MENTION('ai', null, '@ai'),
+ TEXT(', An bot of misskey!'),
+ ])
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('search', () => {
+ describe('検索構文を使用できる', () => {
+ it('Search', () => {
+ const input = 'MFM 書き方 123 Search';
+ const output = [
+ createNode('search', {
+ query: 'MFM 書き方 123',
+ content: input
+ })
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('[Search]', () => {
+ const input = 'MFM 書き方 123 [Search]';
+ const output = [
+ createNode('search', {
+ query: 'MFM 書き方 123',
+ content: input
+ })
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('search', () => {
+ const input = 'MFM 書き方 123 search';
+ const output = [
+ createNode('search', {
+ query: 'MFM 書き方 123',
+ content: input
+ })
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('[search]', () => {
+ const input = 'MFM 書き方 123 [search]';
+ const output = [
+ createNode('search', {
+ query: 'MFM 書き方 123',
+ content: input
+ })
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('検索', () => {
+ const input = 'MFM 書き方 123 検索';
+ const output = [
+ createNode('search', {
+ query: 'MFM 書き方 123',
+ content: input
+ })
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('[検索]', () => {
+ const input = 'MFM 書き方 123 [検索]';
+ const output = [
+ createNode('search', {
+ query: 'MFM 書き方 123',
+ content: input
+ })
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+ it('ブロックの前後にあるテキストが正しく解釈される', () => {
+ const input = 'abc\nhoge piyo bebeyo 検索\n123';
+ const output = [
+ TEXT('abc'),
+ SEARCH('hoge piyo bebeyo', 'hoge piyo bebeyo 検索'),
+ TEXT('123')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('code block', () => {
+ it('コードブロックを使用できる', () => {
+ const input = '```\nabc\n```';
+ const output = [CODE_BLOCK('abc', null)];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('コードブロックには複数行のコードを入力できる', () => {
+ const input = '```\na\nb\nc\n```';
+ const output = [CODE_BLOCK('a\nb\nc', null)];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('コードブロックは言語を指定できる', () => {
+ const input = '```js\nconst a = 1;\n```';
+ const output = [CODE_BLOCK('const a = 1;', 'js')];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('ブロックの前後にあるテキストが正しく解釈される', () => {
+ const input = 'abc\n```\nconst abc = 1;\n```\n123';
+ const output = [
+ TEXT('abc'),
+ CODE_BLOCK('const abc = 1;', null),
+ TEXT('123')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('mathBlock', () => {
+ it('1行の数式ブロックを使用できる', () => {
+ const input = '\\[math1\\]';
+ const output = [
+ MATH_BLOCK('math1')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('ブロックの前後にあるテキストが正しく解釈される', () => {
+ const input = 'abc\n\\[math1\\]\n123';
+ const output = [
+ TEXT('abc'),
+ MATH_BLOCK('math1'),
+ TEXT('123')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('行末以外に閉じタグがある場合はマッチしない', () => {
+ const input = '\\[aaa\\]after';
+ const output = [
+ TEXT('\\[aaa\\]after')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('行頭以外に開始タグがある場合はマッチしない', () => {
+ const input = 'before\\[aaa\\]';
+ const output = [
+ TEXT('before\\[aaa\\]')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('center', () => {
+ it('single text', () => {
+ const input = 'abc';
+ const output = [
+ CENTER([
+ TEXT('abc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('multiple text', () => {
+ const input = 'before\n\nabc\n123\n\npiyo\n\nafter';
+ const output = [
+ TEXT('before'),
+ CENTER([
+ TEXT('abc\n123\n\npiyo')
+ ]),
+ TEXT('after')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('emoji code', () => {
+ it('basic', () => {
+ const input = ':abc:';
+ const output = [EMOJI_CODE('abc')];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('unicode emoji', () => {
+ it('basic', () => {
+ const input = '今起きた😇';
+ const output = [TEXT('今起きた'), UNI_EMOJI('😇')];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('big', () => {
+ it('basic', () => {
+ const input = '***abc***';
+ const output = [
+ FN('tada', { }, [
+ TEXT('abc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('内容にはインライン構文を利用できる', () => {
+ const input = '***123**abc**123***';
+ const output = [
+ FN('tada', { }, [
+ TEXT('123'),
+ BOLD([
+ TEXT('abc')
+ ]),
+ TEXT('123')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('内容は改行できる', () => {
+ const input = '***123\n**abc**\n123***';
+ const output = [
+ FN('tada', { }, [
+ TEXT('123\n'),
+ BOLD([
+ TEXT('abc')
+ ]),
+ TEXT('\n123')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('bold', () => {
+ it('basic', () => {
+ const input = '**abc**';
+ const output = [
+ BOLD([
+ TEXT('abc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('内容にはインライン構文を利用できる', () => {
+ const input = '**123~~abc~~123**';
+ const output = [
+ BOLD([
+ TEXT('123'),
+ STRIKE([
+ TEXT('abc')
+ ]),
+ TEXT('123')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('内容は改行できる', () => {
+ const input = '**123\n~~abc~~\n123**';
+ const output = [
+ BOLD([
+ TEXT('123\n'),
+ STRIKE([
+ TEXT('abc')
+ ]),
+ TEXT('\n123')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('small', () => {
+ it('basic', () => {
+ const input = 'abc';
+ const output = [
+ SMALL([
+ TEXT('abc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('内容にはインライン構文を利用できる', () => {
+ const input = 'abc**123**abc';
+ const output = [
+ SMALL([
+ TEXT('abc'),
+ BOLD([
+ TEXT('123')
+ ]),
+ TEXT('abc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('内容は改行できる', () => {
+ const input = 'abc\n**123**\nabc';
+ const output = [
+ SMALL([
+ TEXT('abc\n'),
+ BOLD([
+ TEXT('123')
+ ]),
+ TEXT('\nabc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('italic 1', () => {
+ it('basic', () => {
+ const input = 'abc';
+ const output = [
+ ITALIC([
+ TEXT('abc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('内容にはインライン構文を利用できる', () => {
+ const input = 'abc**123**abc';
+ const output = [
+ ITALIC([
+ TEXT('abc'),
+ BOLD([
+ TEXT('123')
+ ]),
+ TEXT('abc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ it('内容は改行できる', () => {
+ const input = 'abc\n**123**\nabc';
+ const output = [
+ ITALIC([
+ TEXT('abc\n'),
+ BOLD([
+ TEXT('123')
+ ]),
+ TEXT('\nabc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('italic 2', () => {
+ it('basic', () => {
+ const input = '*abc*';
+ const output = [
+ ITALIC([
+ TEXT('abc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ // strike
+
+ // inlineCode
+
+ // mathInline
+
+ // mention
+
+ describe('hashtag', () => {
+ it('and unicode emoji', () => {
+ const input = '#️⃣abc123#abc';
+ const output = [UNI_EMOJI('#️⃣'), TEXT('abc123'), HASHTAG('abc')];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('url', () => {
+ it('basic', () => {
+ const input = 'official instance: https://misskey.io/@ai.';
+ const output = [
+ TEXT('official instance: '),
+ N_URL('https://misskey.io/@ai'),
+ TEXT('.')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('link', () => {
+ it('basic', () => {
+ const input = '[official instance](https://misskey.io/@ai).';
+ const output = [
+ LINK(false, 'https://misskey.io/@ai', [
+ TEXT('official instance')
+ ]),
+ TEXT('.')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+
+ it('silent flag', () => {
+ const input = '?[official instance](https://misskey.io/@ai).';
+ const output = [
+ LINK(true, 'https://misskey.io/@ai', [
+ TEXT('official instance')
+ ]),
+ TEXT('.')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+
+ it('do not yield url node even if label is recognisable as a url', () => {
+ const input = 'official instance: [https://misskey.io/@ai](https://misskey.io/@ai).';
+ const output = [
+ TEXT('official instance: '),
+ LINK(false, 'https://misskey.io/@ai', [
+ TEXT('https://misskey.io/@ai')
+ ]),
+ TEXT('.')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+
+ it('do not yield link node even if label is recognisable as a link', () => {
+ const input = 'official instance: [[https://misskey.io/@ai](https://misskey.io/@ai)](https://misskey.io/@ai).';
+ const output = [
+ TEXT('official instance: '),
+ LINK(false, 'https://misskey.io/@ai', [
+ TEXT('[https://misskey.io/@ai](https://misskey.io/@ai)')
+ ]),
+ TEXT('.')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ describe('fn', () => {
+ it('basic', () => {
+ const input = '[tada abc]';
+ const output = [
+ FN('tada', { }, [
+ TEXT('abc')
+ ])
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+ });
+
+ it('composite', () => {
+ const input =
+`before
+
+Hello [tada everynyan! 🎉]
+
+I'm @ai, A bot of misskey!
+
+https://github.com/syuilo/ai
+
+after`;
+ const output = [
+ TEXT('before'),
+ CENTER([
+ TEXT('Hello '),
+ FN('tada', { }, [
+ TEXT('everynyan! '),
+ UNI_EMOJI('🎉')
+ ]),
+ TEXT('\n\nI\'m '),
+ MENTION('ai', null, '@ai'),
+ TEXT(', A bot of misskey!\n\n'),
+ N_URL('https://github.com/syuilo/ai')
+ ]),
+ TEXT('after')
+ ];
+ assert.deepStrictEqual(parse(input), output);
+ });
+});