import assert from 'assert';
import * as mfm from '../src/index';
import {
TEXT, CENTER, FN, UNI_EMOJI, MENTION, EMOJI_CODE, HASHTAG, N_URL, BOLD, SMALL, ITALIC, STRIKE, QUOTE, MATH_BLOCK, SEARCH, CODE_BLOCK, LINK, INLINE_CODE, MATH_INLINE, PLAIN
} from '../src/index';
describe('SimpleParser', () => {
describe('text', () => {
test('basic', () => {
const input = 'abc';
const output = [TEXT('abc')];
assert.deepStrictEqual(mfm.parseSimple(input), output);
});
test('ignore hashtag', () => {
const input = 'abc#abc';
const output = [TEXT('abc#abc')];
assert.deepStrictEqual(mfm.parseSimple(input), output);
});
test('keycap number sign', () => {
const input = 'abc#️⃣abc';
const output = [TEXT('abc'), UNI_EMOJI('#️⃣'), TEXT('abc')];
assert.deepStrictEqual(mfm.parseSimple(input), output);
});
});
describe('emoji', () => {
test('basic', () => {
const input = ':foo:';
const output = [EMOJI_CODE('foo')];
assert.deepStrictEqual(mfm.parseSimple(input), output);
});
test('between texts', () => {
const input = 'foo:bar:baz';
const output = [TEXT('foo:bar:baz')];
assert.deepStrictEqual(mfm.parseSimple(input), output);
});
test('between texts 2', () => {
const input = '12:34:56';
const output = [TEXT('12:34:56')];
assert.deepStrictEqual(mfm.parseSimple(input), output);
});
test('between texts 3', () => {
const input = 'あ:bar:い';
const output = [TEXT('あ'), EMOJI_CODE('bar'), TEXT('い')];
assert.deepStrictEqual(mfm.parseSimple(input), output);
});
});
test('disallow other syntaxes', () => {
const input = 'foo **bar** baz';
const output = [TEXT('foo **bar** baz')];
assert.deepStrictEqual(mfm.parseSimple(input), output);
});
});
describe('FullParser', () => {
describe('text', () => {
test('普通のテキストを入力すると1つのテキストノードが返される', () => {
const input = 'abc';
const output = [TEXT('abc')];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('quote', () => {
test('1行の引用ブロックを使用できる', () => {
const input = '> abc';
const output = [
QUOTE([
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('複数行の引用ブロックを使用できる', () => {
const input = `
> abc
> 123
`;
const output = [
QUOTE([
TEXT('abc\n123')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('引用ブロックはブロックをネストできる', () => {
const input = `
>
> a
>
`;
const output = [
QUOTE([
CENTER([
TEXT('a')
])
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('引用ブロックはインライン構文を含んだブロックをネストできる', () => {
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(mfm.parse(input), output);
});
test('複数行の引用ブロックでは空行を含めることができる', () => {
const input = `
> abc
>
> 123
`;
const output = [
QUOTE([
TEXT('abc\n\n123')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('1行の引用ブロックを空行にはできない', () => {
const input = '> ';
const output = [
TEXT('> ')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('引用ブロックの後ろの空行は無視される', () => {
const input = `
> foo
> bar
hoge`;
const output = [
QUOTE([
TEXT('foo\nbar')
]),
TEXT('hoge')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('2つの引用行の間に空行がある場合は2つの引用ブロックが生成される', () => {
const input = `
> foo
> bar
hoge`;
const output = [
QUOTE([
TEXT('foo')
]),
QUOTE([
TEXT('bar')
]),
TEXT('hoge'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('search', () => {
describe('検索構文を使用できる', () => {
test('Search', () => {
const input = 'MFM 書き方 123 Search';
const output = [
TEXT('MFM 書き方 123 Search')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('[Search]', () => {
const input = 'MFM 書き方 123 [Search]';
const output = [
SEARCH('MFM 書き方 123', input)
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('search', () => {
const input = 'MFM 書き方 123 search';
const output = [
TEXT('MFM 書き方 123 search')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('[search]', () => {
const input = 'MFM 書き方 123 [search]';
const output = [
SEARCH('MFM 書き方 123', input)
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('検索', () => {
const input = 'MFM 書き方 123 検索';
const output = [
TEXT('MFM 書き方 123 検索')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('[検索]', () => {
const input = 'MFM 書き方 123 [検索]';
const output = [
SEARCH('MFM 書き方 123', input)
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
test('ブロックの前後にあるテキストが正しく解釈される', () => {
const input = 'abc\nhoge piyo bebeyo 検索\n123';
const output = [
TEXT('abc\nhoge piyo bebeyo 検索\n123')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('code block', () => {
test('コードブロックを使用できる', () => {
const input = '```\nabc\n```';
const output = [CODE_BLOCK('abc', null)];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('コードブロックには複数行のコードを入力できる', () => {
const input = '```\na\nb\nc\n```';
const output = [CODE_BLOCK('a\nb\nc', null)];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('コードブロックは言語を指定できる', () => {
const input = '```js\nconst a = 1;\n```';
const output = [CODE_BLOCK('const a = 1;', 'js')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ブロックの前後にあるテキストが正しく解釈される', () => {
const input = 'abc\n```\nconst abc = 1;\n```\n123';
const output = [
TEXT('abc'),
CODE_BLOCK('const abc = 1;', null),
TEXT('123')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore internal marker', () => {
const input = '```\naaa```bbb\n```';
const output = [CODE_BLOCK('aaa```bbb', null)];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('trim after line break', () => {
const input = '```\nfoo\n```\nbar';
const output = [
CODE_BLOCK('foo', null),
TEXT('bar'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('mathBlock', () => {
test('1行の数式ブロックを使用できる', () => {
const input = '\\[math1\\]';
const output = [
MATH_BLOCK('math1')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ブロックの前後にあるテキストが正しく解釈される', () => {
const input = 'abc\n\\[math1\\]\n123';
const output = [
TEXT('abc'),
MATH_BLOCK('math1'),
TEXT('123')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('行末以外に閉じタグがある場合はマッチする', () => {
const input = '\\[aaa\\]after';
const output = [
MATH_BLOCK('aaa'),
TEXT('after'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('行頭以外に開始タグがある場合はマッチする', () => {
const input = 'before\\[aaa\\]';
const output = [
TEXT('before'),
MATH_BLOCK('aaa'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('center', () => {
test('single text', () => {
const input = 'abc';
const output = [
CENTER([
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('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(mfm.parse(input), output);
});
});
describe('emoji code', () => {
test('basic', () => {
const input = ':abc:';
const output = [EMOJI_CODE('abc')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('non-ASCII', () => {
const input = ':taneŝima_ĝojas:, :मार्जारः:, :鹅:, :taneŝima_malsanas:, :แมว:, and :लक्षणा:';
const output = [
EMOJI_CODE('taneŝima_ĝojas'),
TEXT(', '),
EMOJI_CODE('मार्जारः'),
TEXT(', '),
EMOJI_CODE('鹅'),
TEXT(', '),
EMOJI_CODE('taneŝima_malsanas'),
TEXT(', '),
EMOJI_CODE('แมว'),
TEXT(', and '),
EMOJI_CODE('लक्षणा'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('non-ASCII normalization', () => {
const input = ":fo\u{0308}o:";
const output = [EMOJI_CODE("f\u{00F6}o")];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('unicode emoji', () => {
test('basic', () => {
const input = '今起きた😇';
const output = [TEXT('今起きた'), UNI_EMOJI('😇')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('keycap number sign', () => {
const input = 'abc#️⃣123';
const output = [TEXT('abc'), UNI_EMOJI('#️⃣'), TEXT('123')];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('big', () => {
test('basic', () => {
const input = '***abc***';
const output = [
FN('tada', { }, [
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('内容にはインライン構文を利用できる', () => {
const input = '***123**abc**123***';
const output = [
FN('tada', { }, [
TEXT('123'),
BOLD([
TEXT('abc')
]),
TEXT('123')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('内容は改行できる', () => {
const input = '***123\n**abc**\n123***';
const output = [
FN('tada', { }, [
TEXT('123\n'),
BOLD([
TEXT('abc')
]),
TEXT('\n123')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('bold tag', () => {
test('basic', () => {
const input = 'abc';
const output = [
BOLD([
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('inline syntax allowed inside', () => {
const input = '123~~abc~~123';
const output = [
BOLD([
TEXT('123'),
STRIKE([
TEXT('abc')
]),
TEXT('123')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('line breaks', () => {
const input = '123\n~~abc~~\n123';
const output = [
BOLD([
TEXT('123\n'),
STRIKE([
TEXT('abc')
]),
TEXT('\n123')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('bold', () => {
test('basic', () => {
const input = '**abc**';
const output = [
BOLD([
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('内容にはインライン構文を利用できる', () => {
const input = '**123~~abc~~123**';
const output = [
BOLD([
TEXT('123'),
STRIKE([
TEXT('abc')
]),
TEXT('123')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('内容は改行できる', () => {
const input = '**123\n~~abc~~\n123**';
const output = [
BOLD([
TEXT('123\n'),
STRIKE([
TEXT('abc')
]),
TEXT('\n123')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('small', () => {
test('basic', () => {
const input = 'abc';
const output = [
SMALL([
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('内容にはインライン構文を利用できる', () => {
const input = 'abc**123**abc';
const output = [
SMALL([
TEXT('abc'),
BOLD([
TEXT('123')
]),
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('内容は改行できる', () => {
const input = 'abc\n**123**\nabc';
const output = [
SMALL([
TEXT('abc\n'),
BOLD([
TEXT('123')
]),
TEXT('\nabc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('italic tag', () => {
test('basic', () => {
const input = 'abc';
const output = [
ITALIC([
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('内容にはインライン構文を利用できる', () => {
const input = 'abc**123**abc';
const output = [
ITALIC([
TEXT('abc'),
BOLD([
TEXT('123')
]),
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('内容は改行できる', () => {
const input = 'abc\n**123**\nabc';
const output = [
ITALIC([
TEXT('abc\n'),
BOLD([
TEXT('123')
]),
TEXT('\nabc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('italic alt 1', () => {
test('basic', () => {
const input = '*abc*';
const output = [
ITALIC([
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('basic 2', () => {
const input = 'before *abc* after';
const output = [
TEXT('before '),
ITALIC([
TEXT('abc')
]),
TEXT(' after')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('basic non-ascii', () => {
const input = '*aßc*';
const output = [
ITALIC([
TEXT('aßc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore a italic syntax if the before char is neither a space nor an LF nor [^a-z0-9]i', () => {
let input = 'before*abc*after';
let output: mfm.MfmNode[] = [TEXT('before*abc*after')];
assert.deepStrictEqual(mfm.parse(input), output);
input = 'あいう*abc*えお';
output = [
TEXT('あいう'),
ITALIC([
TEXT('abc')
]),
TEXT('えお')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('italic alt 2', () => {
test('basic', () => {
const input = '_abc_';
const output = [
ITALIC([
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('basic 2', () => {
const input = 'before _abc_ after';
const output = [
TEXT('before '),
ITALIC([
TEXT('abc')
]),
TEXT(' after')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('basic non-ascii', () => {
const input = '_abç_';
const output = [
ITALIC([
TEXT('abç')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore a italic syntax if the before char is neither a space nor an LF nor [^a-z0-9]i', () => {
let input = 'before_abc_after';
let output: mfm.MfmNode[] = [TEXT('before_abc_after')];
assert.deepStrictEqual(mfm.parse(input), output);
input = 'あいう_abc_えお';
output = [
TEXT('あいう'),
ITALIC([
TEXT('abc')
]),
TEXT('えお')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('strike tag', () => {
test('basic', () => {
const input = 'foo';
const output = [STRIKE([
TEXT('foo')
])];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('strike', () => {
test('basic', () => {
const input = '~~foo~~';
const output = [STRIKE([
TEXT('foo')
])];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('basic non-ascii', () => {
const input = '~~föo~~';
const output = [STRIKE([
TEXT('föo')
])];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('inlineCode', () => {
test('basic', () => {
const input = '`var x = "Strawberry Pasta";`';
const output = [INLINE_CODE('var x = "Strawberry Pasta";')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('disallow line break', () => {
const input = '`foo\nbar`';
const output = [TEXT('`foo\nbar`')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('disallow ´', () => {
const input = '`foo´bar`';
const output = [TEXT('`foo´bar`')];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('mathInline', () => {
test('basic', () => {
const input = '\\(x = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}\\)';
const output = [MATH_INLINE('x = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}')];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('mention', () => {
test('basic', () => {
const input = '@abc';
const output = [MENTION('abc', null, '@abc')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('basic 2', () => {
const input = 'before @abc after';
const output = [TEXT('before '), MENTION('abc', null, '@abc'), TEXT(' after')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('basic remote', () => {
const input = '@abc@misskey.io';
const output = [MENTION('abc', 'misskey.io', '@abc@misskey.io')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('basic remote 2', () => {
const input = 'before @abc@misskey.io after';
const output = [TEXT('before '), MENTION('abc', 'misskey.io', '@abc@misskey.io'), TEXT(' after')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('basic remote 3', () => {
const input = 'before\n@abc@misskey.io\nafter';
const output = [TEXT('before\n'), MENTION('abc', 'misskey.io', '@abc@misskey.io'), TEXT('\nafter')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore format of mail address', () => {
const input = 'abc@example.com';
const output = [TEXT('abc@example.com')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('detect as a mention if the before char is [^a-z0-9]i', () => {
const input = 'あいう@abc';
const output = [TEXT('あいう'), MENTION('abc', null, '@abc')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('invalid char only username', () => {
const input = '@-';
const output = [TEXT('@-')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('invalid char only hostname', () => {
const input = '@abc@.';
const output = [TEXT('@abc@.')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('allow "-" in username', () => {
const input = '@abc-d';
const output = [MENTION('abc-d', null, '@abc-d')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('disallow "-" in head of username', () => {
const input = '@-abc';
const output = [TEXT('@-abc')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('disallow "-" in tail of username', () => {
const input = '@abc-';
const output = [MENTION('abc', null, '@abc'), TEXT('-')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('disallow "." in head of hostname', () => {
const input = '@abc@.aaa';
const output = [TEXT('@abc@.aaa')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('disallow "." in tail of hostname', () => {
const input = '@abc@aaa.';
const output = [MENTION('abc', 'aaa', '@abc@aaa'), TEXT('.')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('disallow "-" in head of hostname', () => {
const input = '@abc@-aaa';
const output = [TEXT('@abc@-aaa')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('disallow "-" in tail of hostname', () => {
const input = '@abc@aaa-';
const output = [MENTION('abc', 'aaa', '@abc@aaa'), TEXT('-')];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('hashtag', () => {
test('basic', () => {
const input = '#abc';
const output = [HASHTAG('abc')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('basic 2', () => {
const input = 'before #abc after';
const output = [TEXT('before '), HASHTAG('abc'), TEXT(' after')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('basic non-ascii', () => {
const input = '#äbc';
const output = [HASHTAG('äbc')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('newlines and whitespace', () => {
const input = 'before #abc\nafter #def\u3000foo #ghi\tbar #jkl';
const output = [
TEXT('before '), HASHTAG('abc'),
TEXT('\nafter '), HASHTAG('def'),
TEXT('\u3000foo '), HASHTAG('ghi'),
TEXT('\tbar '), HASHTAG('jkl'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with keycap number sign', () => {
const input = '#️⃣abc123 #abc';
const output = [UNI_EMOJI('#️⃣'), TEXT('abc123 '), HASHTAG('abc')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with keycap number sign 2', () => {
const input = `abc
#️⃣abc`;
const output = [TEXT('abc\n'), UNI_EMOJI('#️⃣'), TEXT('abc')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore a hashtag if the before char is neither a space nor an LF nor [^a-z0-9]i', () => {
let input = 'abc#abc';
let output: mfm.MfmNode[] = [TEXT('abc#abc')];
assert.deepStrictEqual(mfm.parse(input), output);
input = 'あいう#abc';
output = [TEXT('あいう'), HASHTAG('abc')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore comma and period', () => {
const input = 'Foo #bar, baz #piyo.';
const output = [TEXT('Foo '), HASHTAG('bar'), TEXT(', baz '), HASHTAG('piyo'), TEXT('.')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore exclamation mark', () => {
const input = '#Foo!';
const output = [HASHTAG('Foo'), TEXT('!')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore colon', () => {
const input = '#Foo:';
const output = [HASHTAG('Foo'), TEXT(':')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore single quote', () => {
const input = '#Foo\'';
const output = [HASHTAG('Foo'), TEXT('\'')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore double quote', () => {
const input = '#Foo"';
const output = [HASHTAG('Foo'), TEXT('"')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore square bracket', () => {
const input = '#Foo]';
const output = [HASHTAG('Foo'), TEXT(']')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore slash', () => {
const input = '#foo/bar';
const output = [HASHTAG('foo'), TEXT('/bar')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore angle bracket', () => {
const input = '#foo';
const output = [HASHTAG('foo'), TEXT('')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('allow including number', () => {
const input = '#foo123';
const output = [HASHTAG('foo123')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with brackets "()"', () => {
const input = '(#foo)';
const output = [TEXT('('), HASHTAG('foo'), TEXT(')')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with brackets "「」"', () => {
const input = '「#foo」';
const output = [TEXT('「'), HASHTAG('foo'), TEXT('」')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with mixed brackets', () => {
const input = '「#foo(bar)」';
const output = [TEXT('「'), HASHTAG('foo(bar)'), TEXT('」')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with brackets "()" (space before)', () => {
const input = '(bar #foo)';
const output = [TEXT('(bar '), HASHTAG('foo'), TEXT(')')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with brackets "「」" (space before)', () => {
const input = '「bar #foo」';
const output = [TEXT('「bar '), HASHTAG('foo'), TEXT('」')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('disallow number only', () => {
const input = '#123';
const output = [TEXT('#123')];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('disallow number only (with brackets)', () => {
const input = '(#123)';
const output = [TEXT('(#123)')];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('url', () => {
test('basic', () => {
const input = 'https://misskey.io/@ai';
const output = [
N_URL('https://misskey.io/@ai'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with other texts', () => {
const input = 'official instance: https://misskey.io/@ai.';
const output = [
TEXT('official instance: '),
N_URL('https://misskey.io/@ai'),
TEXT('.')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore trailing period', () => {
const input = 'https://misskey.io/@ai.';
const output = [
N_URL('https://misskey.io/@ai'),
TEXT('.')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('disallow period only', () => {
const input = 'https://.';
const output = [
TEXT('https://.')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore trailing periods', () => {
const input = 'https://misskey.io/@ai...';
const output = [
N_URL('https://misskey.io/@ai'),
TEXT('...')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with comma', () => {
const input = 'https://example.com/foo?bar=a,b';
const output = [
N_URL('https://example.com/foo?bar=a,b'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore trailing comma', () => {
const input = 'https://example.com/foo, bar';
const output = [
N_URL('https://example.com/foo'),
TEXT(', bar')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with brackets', () => {
const input = 'https://example.com/foo(bar)';
const output = [
N_URL('https://example.com/foo(bar)'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore parent brackets', () => {
const input = '(https://example.com/foo)';
const output = [
TEXT('('),
N_URL('https://example.com/foo'),
TEXT(')'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore parent brackets (2)', () => {
const input = '(foo https://example.com/foo)';
const output = [
TEXT('(foo '),
N_URL('https://example.com/foo'),
TEXT(')'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore parent brackets with internal brackets', () => {
const input = '(https://example.com/foo(bar))';
const output = [
TEXT('('),
N_URL('https://example.com/foo(bar)'),
TEXT(')'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore parent []', () => {
const input = 'foo [https://example.com/foo] bar';
const output = [
TEXT('foo ['),
N_URL('https://example.com/foo'),
TEXT('] bar'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('ignore non-ascii characters contained url without angle brackets', () => {
const input = 'https://大石泉すき.example.com';
const output = [
TEXT('https://大石泉すき.example.com'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('match non-ascii characters contained url with angle brackets', () => {
const input = '';
const output = [
N_URL('https://大石泉すき.example.com', true),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('prevent xss', () => {
const input = 'javascript:foo';
const output = [
TEXT('javascript:foo')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('link', () => {
test('basic', () => {
const input = '[official instance](https://misskey.io/@ai).';
const output = [
LINK(false, 'https://misskey.io/@ai', [
TEXT('official instance')
]),
TEXT('.')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('silent flag', () => {
const input = '?[official instance](https://misskey.io/@ai).';
const output = [
LINK(true, 'https://misskey.io/@ai', [
TEXT('official instance')
]),
TEXT('.')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with angle brackets url', () => {
const input = '[official instance]().';
const output = [
LINK(false, 'https://misskey.io/@ai', [
TEXT('official instance')
]),
TEXT('.')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('prevent xss', () => {
const input = '[click here](javascript:foo)';
const output = [
TEXT('[click here](javascript:foo)')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
describe('cannot nest a url in a link label', () => {
test('basic', () => {
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(mfm.parse(input), output);
});
test('nested', () => {
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'),
BOLD([
TEXT('https://misskey.io/@ai'),
]),
]),
TEXT('.'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('cannot nest a link in a link label', () => {
test('basic', () => {
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'),
]),
TEXT(']('),
N_URL('https://misskey.io/@ai'),
TEXT(').'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('nested', () => {
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', [
BOLD([
TEXT('[https://misskey.io/@ai](https://misskey.io/@ai)'),
]),
]),
TEXT('.'),
];
});
});
describe('cannot nest a mention in a link label', () => {
test('basic', () => {
const input = '[@example](https://example.com)';
const output = [
LINK(false, 'https://example.com', [
TEXT('@example'),
]),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('nested', () => {
const input = '[@example**@example**](https://example.com)';
const output = [
LINK(false, 'https://example.com', [
TEXT('@example'),
BOLD([
TEXT('@example'),
]),
]),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
test('with brackets', () => {
const input = '[foo](https://example.com/foo(bar))';
const output = [
LINK(false, 'https://example.com/foo(bar)', [
TEXT('foo')
]),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with parent brackets', () => {
const input = '([foo](https://example.com/foo(bar)))';
const output = [
TEXT('('),
LINK(false, 'https://example.com/foo(bar)', [
TEXT('foo')
]),
TEXT(')'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with brackets before', () => {
const input = '[test] foo [bar](https://example.com)';
const output = [
TEXT('[test] foo '),
LINK(false, 'https://example.com', [
TEXT('bar')
]),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('bad url in url part', () => {
const input = "[test](http://..)";
const output = [
TEXT("[test](http://..)")
];
assert.deepStrictEqual(mfm.parse(input), output);
})
});
describe('fn', () => {
test('basic', () => {
const input = '$[tada abc]';
const output = [
FN('tada', { }, [
TEXT('abc')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with a string argument', () => {
const input = '$[spin.speed=1.1s a]';
const output = [
FN('spin', { speed: '1.1s' }, [
TEXT('a')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('with a string argument 2', () => {
const input = '$[position.x=-3 a]';
const output = [
FN('position', { x: '-3' }, [
TEXT('a')
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('invalid fn name', () => {
const input = '$[関数 text]';
const output = [
TEXT('$[関数 text]')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('nest', () => {
const input = '$[spin.speed=1.1s $[shake a]]';
const output = [
FN('spin', { speed: '1.1s' }, [
FN('shake', { }, [
TEXT('a')
])
])
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('plain', () => {
test('multiple line', () => {
const input = 'a\n\n**Hello**\nworld\n\nb';
const output = [
TEXT('a\n'),
PLAIN('**Hello**\nworld'),
TEXT('\nb')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('single line', () => {
const input = 'a\n**Hello** world\nb';
const output = [
TEXT('a\n'),
PLAIN('**Hello** world'),
TEXT('\nb')
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
describe('nesting limit', () => {
describe('quote', () => {
test('basic', () => {
const input = '>>> abc';
const output = [
QUOTE([
QUOTE([
TEXT('> abc'),
]),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
test('basic 2', () => {
const input = '>> **abc**';
const output = [
QUOTE([
QUOTE([
TEXT('**abc**'),
]),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
});
test('big', () => {
const input = '***abc***';
const output = [
BOLD([
BOLD([
TEXT('***abc***'),
]),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
describe('bold', () => {
test('basic', () => {
const input = '**abc**';
const output = [
ITALIC([
ITALIC([
TEXT('**abc**'),
]),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
test('tag', () => {
const input = 'abc';
const output = [
ITALIC([
ITALIC([
TEXT('abc'),
]),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
});
test('small', () => {
const input = 'abc';
const output = [
ITALIC([
ITALIC([
TEXT('abc'),
]),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
test('italic', () => {
const input = 'abc';
const output = [
BOLD([
BOLD([
TEXT('abc'),
]),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
describe('strike', () => {
test('basic', () => {
const input = '~~abc~~';
const output = [
BOLD([
BOLD([
TEXT('~~abc~~'),
]),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
test('tag', () => {
const input = 'abc';
const output = [
BOLD([
BOLD([
TEXT('abc'),
]),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
});
describe('hashtag', () => {
test('basic', () => {
let input, output;
input = '#abc(xyz)';
output = [
BOLD([
HASHTAG('abc(xyz)'),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
input = '#abc(x(y)z)';
output = [
BOLD([
HASHTAG('abc'),
TEXT('(x(y)z)'),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
test('outside "()"', () => {
const input = '(#abc)';
const output = [
TEXT('('),
HASHTAG('abc'),
TEXT(')'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('outside "[]"', () => {
const input = '[#abc]';
const output = [
TEXT('['),
HASHTAG('abc'),
TEXT(']'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('outside "「」"', () => {
const input = '「#abc」';
const output = [
TEXT('「'),
HASHTAG('abc'),
TEXT('」'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
test('outside "()"', () => {
const input = '(#abc)';
const output = [
TEXT('('),
HASHTAG('abc'),
TEXT(')'),
];
assert.deepStrictEqual(mfm.parse(input), output);
});
});
test('url', () => {
let input, output;
input = 'https://example.com/abc(xyz)';
output = [
BOLD([
N_URL('https://example.com/abc(xyz)'),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
input = 'https://example.com/abc(x(y)z)';
output = [
BOLD([
N_URL('https://example.com/abc'),
TEXT('(x(y)z)'),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
test('fn', () => {
const input = '$[a b]';
const output = [
BOLD([
BOLD([
TEXT('$[a b]'),
]),
]),
];
assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output);
});
});
test('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(mfm.parse(input), output);
});
});