From e7f70c69d029fb2e7a5cea1711f934bd58d029fe Mon Sep 17 00:00:00 2001 From: shibao Date: Sun, 5 Feb 2023 11:08:49 -0500 Subject: [PATCH] Use MfmUrl AST in MfmLink AST --- CHANGELOG.md | 4 ++ docs/syntax.md | 33 ++++++++---- etc/mfm-js.api.md | 4 +- package.json | 2 +- src/internal/parser.ts | 2 +- src/internal/util.ts | 2 +- src/node.ts | 4 +- test/api.ts | 7 ++- test/parser.ts | 118 ++++++++++++++++++++++++++--------------- 9 files changed, 113 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 263361d..e4887d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ ### Bugfixes --> +## 0.23.4 (unreleased) + +### Improvements +- Improve generation of brackets for links (#126) ## 0.23.3 - tweak fn parsing diff --git a/docs/syntax.md b/docs/syntax.md index 29ae576..763ed72 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -225,10 +225,10 @@ __bold__ - 内容には再度InlineParserを適用する。 - 内容を空にすることはできない。 -構文1,3のみ: +構文1,3のみ: - 内容にはすべての文字、改行が使用できる。 -構文2のみ: +構文2のみ: - 内容には`[a-z0-9 \t]i`にマッチする文字が使用できる。 ## ノード @@ -289,11 +289,11 @@ _italic_ - 内容には再度InlineParserを適用する。 - 内容を空にすることはできない。 -構文1のみ: +構文1のみ: - 内容にはすべての文字、改行が使用できる。 -構文2,3のみ: -※1つ目の`*`と`_`を開始記号と呼ぶ。 +構文2,3のみ: +※1つ目の`*`と`_`を開始記号と呼ぶ。 - 内容には`[a-z0-9 \t]i`にマッチする文字が使用できる。 - 開始記号の前の文字が`[a-z0-9]i`に一致しない時にイタリック文字として判定される。 @@ -326,10 +326,10 @@ _italic_ - 内容には再度InlineParserを適用する。 - 内容を空にすることはできない。 -構文1のみ: +構文1のみ: - 内容には`~`、改行以外の文字を使用できる。 -構文2のみ: +構文2のみ: - 内容にはすべての文字、改行が使用できる。 ## ノード @@ -488,12 +488,12 @@ http://hoge.jp/abc ``` ## 詳細 -構文1のみ: +構文1のみ: - 内容には`[.,a-z0-9_/:%#@$&?!~=+-]i`にマッチする文字を使用できる。 - 内容には対になっている括弧を使用できる。対象: `( )` `[ ]` - `.`や`,`は最後の文字にできない。 -構文2のみ: +構文2のみ: - 内容には改行、スペース以外の文字を使用できる。 ## ノード @@ -545,6 +545,11 @@ silent=true ?[Misskey.io](https://misskey.io/) ``` +Special characters +``` +[#藍ちゃファンクラブ]() +``` + ## 詳細 - 表示テキストには再度InlineParserを適用する。ただし、表示テキストではURL、リンク、メンションは使用できない。 @@ -555,7 +560,13 @@ silent=true type: 'link', props: { silent: false, - url: 'https://misskey.io/' + url: { + type: 'url', + props: { + url: 'https://misskey.io/@ai', + brackets: false + } + }, }, children: [ { @@ -665,7 +676,7 @@ abc ```js { type: 'text', - props: + props: text: 'abc' } } diff --git a/etc/mfm-js.api.md b/etc/mfm-js.api.md index 6792d39..385e1f9 100644 --- a/etc/mfm-js.api.md +++ b/etc/mfm-js.api.md @@ -38,7 +38,7 @@ export function inspect(nodes: MfmNode[], action: (node: MfmNode) => void): void export const ITALIC: (children: MfmInline[]) => NodeType<'italic'>; // @public (undocumented) -export const LINK: (silent: boolean, url: string, children: MfmInline[]) => NodeType<'link'>; +export const LINK: (silent: boolean, url: MfmUrl, children: MfmInline[]) => NodeType<'link'>; // @public (undocumented) export const MATH_BLOCK: (formula: string) => NodeType<'mathBlock'>; @@ -128,7 +128,7 @@ export type MfmLink = { type: 'link'; props: { silent: boolean; - url: string; + url: MfmUrl; }; children: MfmInline[]; }; diff --git a/package.json b/package.json index a66c7b9..3da31c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mfm-js", - "version": "0.23.3", + "version": "0.23.4", "description": "An MFM parser implementation with TypeScript", "main": "./built/index.js", "types": "./built/index.d.ts", diff --git a/src/internal/parser.ts b/src/internal/parser.ts index 0d19f27..342f5e7 100644 --- a/src/internal/parser.ts +++ b/src/internal/parser.ts @@ -657,7 +657,7 @@ export const language = P.createLanguage({ const silent = (result[1] === '?['); const label = result[2]; const url: M.MfmUrl = result[5]; - return M.LINK(silent, url.props.url, mergeText(label)); + return M.LINK(silent, url, mergeText(label)); }); }, diff --git a/src/internal/util.ts b/src/internal/util.ts index 3f0cffc..d043429 100644 --- a/src/internal/util.ts +++ b/src/internal/util.ts @@ -92,7 +92,7 @@ export function stringifyNode(node: MfmNode): string { } case 'link': { const prefix = node.props.silent ? '?' : ''; - return `${ prefix }[${ stringifyTree(node.children) }](${ node.props.url })`; + return `${ prefix }[${ stringifyTree(node.children) }](${ stringifyNode(node.props.url) })`; } case 'fn': { const argFields = Object.keys(node.props.args).map(key => { diff --git a/src/node.ts b/src/node.ts index 2f00438..7942e96 100644 --- a/src/node.ts +++ b/src/node.ts @@ -157,11 +157,11 @@ export type MfmLink = { type: 'link'; props: { silent: boolean; - url: string; + url: MfmUrl; }; children: MfmInline[]; }; -export const LINK = (silent: boolean, url: string, children: MfmInline[]): NodeType<'link'> => { return { type: 'link', props: { silent, url }, children }; }; +export const LINK = (silent: boolean, url: MfmUrl, children: MfmInline[]): NodeType<'link'> => { return { type: 'link', props: { silent, url }, children }; }; export type MfmFn = { type: 'fn'; diff --git a/test/api.ts b/test/api.ts index 25b29e2..0f6bd0b 100644 --- a/test/api.ts +++ b/test/api.ts @@ -28,7 +28,7 @@ after`; test('quote', () => { const input = ` > abc -> +> > 123 `; @@ -132,6 +132,11 @@ after`; assert.strictEqual(mfm.toString(mfm.parse(input)), '[Ai](https://github.com/syuilo/ai)'); }); + test('bracket link', () => { + const input = '[#藍ちゃファンクラブ]()'; + assert.strictEqual(mfm.toString(mfm.parse(input)), '[#藍ちゃファンクラブ]()'); + }); + test('silent link', () => { const input = '?[Ai](https://github.com/syuilo/ai)'; assert.strictEqual(mfm.toString(mfm.parse(input)), '?[Ai](https://github.com/syuilo/ai)'); diff --git a/test/parser.ts b/test/parser.ts index 50ae700..fc2a9d6 100644 --- a/test/parser.ts +++ b/test/parser.ts @@ -1042,9 +1042,11 @@ hoge`; test('basic', () => { const input = '[official instance](https://misskey.io/@ai).'; const output = [ - LINK(false, 'https://misskey.io/@ai', [ - TEXT('official instance') - ]), + LINK( + false, + N_URL('https://misskey.io/@ai'), + [TEXT('official instance')] + ), TEXT('.') ]; assert.deepStrictEqual(mfm.parse(input), output); @@ -1053,20 +1055,24 @@ hoge`; test('silent flag', () => { const input = '?[official instance](https://misskey.io/@ai).'; const output = [ - LINK(true, 'https://misskey.io/@ai', [ - TEXT('official instance') - ]), + LINK( + true, + N_URL('https://misskey.io/@ai'), + [TEXT('official instance')] + ), TEXT('.') ]; assert.deepStrictEqual(mfm.parse(input), output); }); test('with angle brackets url', () => { - const input = '[official instance]().'; + const input = '[#藍ちゃファンクラブ]().'; const output = [ - LINK(false, 'https://misskey.io/@ai', [ - TEXT('official instance') - ]), + LINK( + false, + N_URL('https://misskey.io/explore/tags/藍ちゃファンクラブ', true), + [TEXT('#藍ちゃファンクラブ')] + ), TEXT('.') ]; assert.deepStrictEqual(mfm.parse(input), output); @@ -1085,9 +1091,11 @@ hoge`; 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'), - ]), + LINK( + false, + N_URL('https://misskey.io/@ai'), + [TEXT('https://misskey.io/@ai')] + ), TEXT('.'), ]; assert.deepStrictEqual(mfm.parse(input), output); @@ -1096,12 +1104,16 @@ hoge`; 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([ + LINK( + false, + N_URL('https://misskey.io/@ai'), + [ TEXT('https://misskey.io/@ai'), - ]), - ]), + BOLD([ + TEXT('https://misskey.io/@ai'), + ]), + ] + ), TEXT('.'), ]; assert.deepStrictEqual(mfm.parse(input), output); @@ -1113,9 +1125,11 @@ hoge`; 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'), - ]), + LINK( + false, + N_URL('https://misskey.io/@ai'), + [TEXT('[https://misskey.io/@ai')] + ), TEXT(']('), N_URL('https://misskey.io/@ai'), TEXT(').'), @@ -1126,11 +1140,15 @@ hoge`; 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)'), - ]), - ]), + LINK( + false, + N_URL('https://misskey.io/@ai'), + [ + BOLD([ + TEXT('[https://misskey.io/@ai](https://misskey.io/@ai)'), + ]), + ] + ), TEXT('.'), ]; }); @@ -1140,21 +1158,27 @@ hoge`; test('basic', () => { const input = '[@example](https://example.com)'; const output = [ - LINK(false, 'https://example.com', [ - TEXT('@example'), - ]), + LINK( + false, + N_URL('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([ + LINK( + false, + N_URL('https://example.com'), + [ TEXT('@example'), - ]), - ]), + BOLD([ + TEXT('@example'), + ]), + ] + ), ]; assert.deepStrictEqual(mfm.parse(input), output); }); @@ -1163,9 +1187,11 @@ hoge`; test('with brackets', () => { const input = '[foo](https://example.com/foo(bar))'; const output = [ - LINK(false, 'https://example.com/foo(bar)', [ - TEXT('foo') - ]), + LINK( + false, + N_URL('https://example.com/foo(bar)'), + [TEXT('foo')] + ), ]; assert.deepStrictEqual(mfm.parse(input), output); }); @@ -1174,9 +1200,11 @@ hoge`; const input = '([foo](https://example.com/foo(bar)))'; const output = [ TEXT('('), - LINK(false, 'https://example.com/foo(bar)', [ - TEXT('foo') - ]), + LINK( + false, + N_URL('https://example.com/foo(bar)'), + [TEXT('foo')] + ), TEXT(')'), ]; assert.deepStrictEqual(mfm.parse(input), output); @@ -1186,9 +1214,11 @@ hoge`; const input = '[test] foo [bar](https://example.com)'; const output = [ TEXT('[test] foo '), - LINK(false, 'https://example.com', [ - TEXT('bar') - ]), + LINK( + false, + N_URL('https://example.com'), + [TEXT('bar')] + ), ]; assert.deepStrictEqual(mfm.parse(input), output); }); @@ -1369,7 +1399,7 @@ hoge`; ]; assert.deepStrictEqual(mfm.parse(input, { nestLimit: 2 }), output); }); - + test('tag', () => { const input = 'abc'; const output = [