From 9d5277b57562f7296e992f6e2baf502bc37532d8 Mon Sep 17 00:00:00 2001 From: shibao Date: Sat, 15 Oct 2022 13:53:25 -0400 Subject: [PATCH] add support for additional link modes --- .tool-versions | 1 + CHANGELOG.md | 5 ++++ docs/syntax.md | 17 +++++++++--- etc/mfm-js.api.md | 4 +-- package.json | 2 +- src/internal/parser.ts | 13 ++++++--- src/internal/util.ts | 10 +++++-- src/node.ts | 4 +-- test/api.ts | 15 ++++++++++ test/parser.ts | 63 ++++++++++++++++++++++++++++++++++-------- 10 files changed, 106 insertions(+), 28 deletions(-) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..1237b21 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 16.13.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index c01d87e..720ac1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ ### Bugfixes --> +## 0.24.0 + +### Features +- Add Remote media syntax for links (#127) + ## 0.23.1 ### Improvements diff --git a/docs/syntax.md b/docs/syntax.md index ec51873..903c7d2 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -535,20 +535,29 @@ http://hoge.jp/abc

Inline: リンク

## 形式 -silent=false +type='plain' ``` [Misskey.io](https://misskey.io/) ``` -silent=true +type='plain' with special characters +``` +[#藍ちゃファンクラブ]() +``` + +type='silent' ``` ?[Misskey.io](https://misskey.io/) ``` +type='embed' +``` +![Misskey.io](https://misskey.io/) +``` + Special characters ``` [#藍ちゃファンクラブ]() -``` ## 詳細 - リンクラベルには再度InlineParserを適用する。ただし、リンクラベルではURL、リンク、メンションは使用できない。 @@ -559,7 +568,7 @@ Special characters { type: 'link', props: { - silent: false, + type: 'plain' url: { type: 'url', props: { diff --git a/etc/mfm-js.api.md b/etc/mfm-js.api.md index 6b7517a..5908bf6 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: MfmUrl, children: MfmInline[]) => NodeType<'link'>; +export const LINK: (type: 'plain' | 'silent' | 'embed', url: MfmUrl, children: MfmInline[]) => NodeType<'link'>; // @public (undocumented) export const MATH_BLOCK: (formula: string) => NodeType<'mathBlock'>; @@ -127,7 +127,7 @@ export type MfmItalic = { export type MfmLink = { type: 'link'; props: { - silent: boolean; + type: 'plain' | 'silent' | 'embed'; url: MfmUrl; }; children: MfmInline[]; diff --git a/package.json b/package.json index d79f770..92c275d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mfm-js", - "version": "0.23.1", + "version": "0.24.0", "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 4c9a058..328df88 100644 --- a/src/internal/parser.ts +++ b/src/internal/parser.ts @@ -465,7 +465,7 @@ export const language = P.createLanguage({ P.str('.'), arg.sep(P.str(','), 1), ], 1).map(pairs => { - const result: Args = { }; + const result: Args = {}; for (const pair of pairs) { result[pair.k] = pair.v; } @@ -644,7 +644,7 @@ export const language = P.createLanguage({ const closeLabel = P.str(']'); return P.seq([ notLinkLabel, - P.alt([P.str('?['), P.str('[')]), + P.alt([P.str('?['), P.str('!['), P.str('[')]), P.seq([ P.notMatch(P.alt([closeLabel, newLine])), nest(labelInline), @@ -654,10 +654,15 @@ export const language = P.createLanguage({ P.alt([r.urlAlt, r.url]), P.str(')'), ]).map(result => { - const silent = (result[1] === '?['); + const mapping: {[key: string]: M.MfmLink['props']['type']} = { + '?[': 'silent', + '![': 'embed', + '[': 'plain', + }; + const type: M.MfmLink['props']['type'] = mapping[result[1]]; const label = result[2]; const url: M.MfmUrl = result[5]; - return M.LINK(silent, url, mergeText(label)); + return M.LINK(type, url, mergeText(label)); }); }, diff --git a/src/internal/util.ts b/src/internal/util.ts index d043429..079afd3 100644 --- a/src/internal/util.ts +++ b/src/internal/util.ts @@ -1,4 +1,4 @@ -import { isMfmBlock, MfmInline, MfmNode, MfmText, TEXT } from '../node'; +import { isMfmBlock, MfmInline, MfmNode, MfmText, MfmLink, TEXT } from '../node'; export function mergeText(nodes: ((T extends MfmInline ? MfmInline : MfmNode) | string)[]): (T | MfmText)[] { const dest: (T | MfmText)[] = []; @@ -91,8 +91,12 @@ export function stringifyNode(node: MfmNode): string { } } case 'link': { - const prefix = node.props.silent ? '?' : ''; - return `${ prefix }[${ stringifyTree(node.children) }](${ stringifyNode(node.props.url) })`; + const prefixMapping: {[key in MfmLink['props']['type']]: string} = { + 'silent': '?', + 'embed': '!', + 'plain': '', + }; + return `${ prefixMapping[node.props.type] }[${ 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 7942e96..042f825 100644 --- a/src/node.ts +++ b/src/node.ts @@ -156,12 +156,12 @@ export const N_URL = (value: string, brackets?: boolean): NodeType<'url'> => { export type MfmLink = { type: 'link'; props: { - silent: boolean; + type: 'plain' | 'silent' | 'embed'; url: MfmUrl; }; children: MfmInline[]; }; -export const LINK = (silent: boolean, url: MfmUrl, children: MfmInline[]): NodeType<'link'> => { return { type: 'link', props: { silent, url }, children }; }; +export const LINK = (type: 'plain' | 'silent' | 'embed', url: MfmUrl, children: MfmInline[]): NodeType<'link'> => { return { type: 'link', props: { type, url }, children }; }; export type MfmFn = { type: 'fn'; diff --git a/test/api.ts b/test/api.ts index 94a10b3..6bdc95e 100644 --- a/test/api.ts +++ b/test/api.ts @@ -142,6 +142,21 @@ after`; assert.strictEqual(mfm.toString(mfm.parse(input)), '?[Ai](https://github.com/syuilo/ai)'); }); + it('silent bracket link', () => { + const input = '?[#藍ちゃファンクラブ]()'; + assert.strictEqual(mfm.toString(mfm.parse(input)), '?[#藍ちゃファンクラブ]()'); + }); + + it('image link', () => { + const input = '![Ai logo](https://raw.githubusercontent.com/syuilo/ai/master/ai.svg)'; + assert.strictEqual(mfm.toString(mfm.parse(input)), '![Ai logo](https://raw.githubusercontent.com/syuilo/ai/master/ai.svg)'); + }); + + it('image bracket link', () => { + const input = '![#藍ちゃファンクラブ]()'; + assert.strictEqual(mfm.toString(mfm.parse(input)), '![#藍ちゃファンクラブ]()'); + }); + it('fn', () => { const input = '$[tada Hello]'; assert.strictEqual(mfm.toString(mfm.parse(input)), '$[tada Hello]'); diff --git a/test/parser.ts b/test/parser.ts index 13f6a0c..e1c1e52 100644 --- a/test/parser.ts +++ b/test/parser.ts @@ -1023,7 +1023,7 @@ hoge`; const input = '[official instance](https://misskey.io/@ai).'; const output = [ LINK( - false, + 'plain', N_URL('https://misskey.io/@ai'), [TEXT('official instance')] ), @@ -1036,7 +1036,7 @@ hoge`; const input = '?[official instance](https://misskey.io/@ai).'; const output = [ LINK( - true, + 'silent', N_URL('https://misskey.io/@ai'), [TEXT('official instance')] ), @@ -1049,7 +1049,7 @@ hoge`; const input = '[#藍ちゃファンクラブ]().'; const output = [ LINK( - false, + 'plain', N_URL('https://misskey.io/explore/tags/藍ちゃファンクラブ', true), [TEXT('#藍ちゃファンクラブ')] ), @@ -1058,13 +1058,52 @@ hoge`; assert.deepStrictEqual(mfm.parse(input), output); }); + it('embed flag', () => { + const input = '![image](https://raw.githubusercontent.com/syuilo/ai/master/ai.svg).'; + const output = [ + LINK( + 'embed', + N_URL('https://raw.githubusercontent.com/syuilo/ai/master/ai.svg'), + [TEXT('image')] + ), + TEXT('.') + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + it('with angle brackets silent url', () => { + const input = '?[image]().'; + const output = [ + LINK( + 'silent', + N_URL('https://raw.githubusercontent.com/syuilo/ai/master/ai.svg', true), + [TEXT('image')] + ), + TEXT('.') + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + + it('with angle brackets embed url', () => { + const input = '![image]().'; + const output = [ + LINK( + 'embed', + N_URL('https://raw.githubusercontent.com/syuilo/ai/master/ai.svg', true), + [TEXT('image')] + ), + TEXT('.') + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }); + describe('cannot nest a url in a link label', () => { it('basic', () => { const input = 'official instance: [https://misskey.io/@ai](https://misskey.io/@ai).'; const output = [ TEXT('official instance: '), LINK( - false, + 'plain', N_URL('https://misskey.io/@ai'), [TEXT('https://misskey.io/@ai')] ), @@ -1077,7 +1116,7 @@ hoge`; const output = [ TEXT('official instance: '), LINK( - false, + 'plain', N_URL('https://misskey.io/@ai'), [ TEXT('https://misskey.io/@ai'), @@ -1098,7 +1137,7 @@ hoge`; const output = [ TEXT('official instance: '), LINK( - false, + 'plain', N_URL('https://misskey.io/@ai'), [TEXT('[https://misskey.io/@ai')] ), @@ -1113,7 +1152,7 @@ hoge`; const output = [ TEXT('official instance: '), LINK( - false, + 'plain', N_URL('https://misskey.io/@ai'), [ BOLD([ @@ -1131,7 +1170,7 @@ hoge`; const input = '[@example](https://example.com)'; const output = [ LINK( - false, + 'plain', N_URL('https://example.com'), [TEXT('@example')] ), @@ -1142,7 +1181,7 @@ hoge`; const input = '[@example**@example**](https://example.com)'; const output = [ LINK( - false, + 'plain', N_URL('https://example.com'), [ TEXT('@example'), @@ -1160,7 +1199,7 @@ hoge`; const input = '[foo](https://example.com/foo(bar))'; const output = [ LINK( - false, + 'plain', N_URL('https://example.com/foo(bar)'), [TEXT('foo')] ), @@ -1173,7 +1212,7 @@ hoge`; const output = [ TEXT('('), LINK( - false, + 'plain', N_URL('https://example.com/foo(bar)'), [TEXT('foo')] ), @@ -1187,7 +1226,7 @@ hoge`; const output = [ TEXT('[test] foo '), LINK( - false, + 'plain', N_URL('https://example.com'), [TEXT('bar')] ),