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 e4887d4..36856b7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,9 @@
-->
## 0.23.4 (unreleased)
+### Features
+- Add remote media syntax for links (#127)
+
### Improvements
- Improve generation of brackets for links (#126)
diff --git a/docs/syntax.md b/docs/syntax.md
index 763ed72..698c7f9 100644
--- a/docs/syntax.md
+++ b/docs/syntax.md
@@ -535,19 +535,24 @@ 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/)
```
-Special characters
+type='embed'
```
-[#藍ちゃファンクラブ]()
+![Misskey.io](https://misskey.io/)
```
## 詳細
@@ -559,7 +564,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 385e1f9..444d604 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/src/internal/parser.ts b/src/internal/parser.ts
index 342f5e7..3385e8b 100644
--- a/src/internal/parser.ts
+++ b/src/internal/parser.ts
@@ -462,7 +462,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 0f6bd0b..3c97202 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)');
});
+ test('silent bracket link', () => {
+ const input = '?[#藍ちゃファンクラブ]()';
+ assert.strictEqual(mfm.toString(mfm.parse(input)), '?[#藍ちゃファンクラブ]()');
+ });
+
+ test('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)');
+ });
+
+ test('image bracket link', () => {
+ const input = '![#藍ちゃファンクラブ]()';
+ assert.strictEqual(mfm.toString(mfm.parse(input)), '![#藍ちゃファンクラブ]()');
+ });
+
test('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 fc2a9d6..b46fb3d 100644
--- a/test/parser.ts
+++ b/test/parser.ts
@@ -1043,7 +1043,7 @@ hoge`;
const input = '[official instance](https://misskey.io/@ai).';
const output = [
LINK(
- false,
+ 'plain',
N_URL('https://misskey.io/@ai'),
[TEXT('official instance')]
),
@@ -1056,7 +1056,7 @@ hoge`;
const input = '?[official instance](https://misskey.io/@ai).';
const output = [
LINK(
- true,
+ 'silent',
N_URL('https://misskey.io/@ai'),
[TEXT('official instance')]
),
@@ -1069,7 +1069,7 @@ hoge`;
const input = '[#藍ちゃファンクラブ]().';
const output = [
LINK(
- false,
+ 'plain',
N_URL('https://misskey.io/explore/tags/藍ちゃファンクラブ', true),
[TEXT('#藍ちゃファンクラブ')]
),
@@ -1086,13 +1086,52 @@ hoge`;
assert.deepStrictEqual(mfm.parse(input), output);
});
+ test('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);
+ });
+
+ test('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);
+ });
+
+ test('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', () => {
test('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')]
),
@@ -1100,12 +1139,13 @@ hoge`;
];
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,
+ 'plain',
N_URL('https://misskey.io/@ai'),
[
TEXT('https://misskey.io/@ai'),
@@ -1126,7 +1166,7 @@ hoge`;
const output = [
TEXT('official instance: '),
LINK(
- false,
+ 'plain',
N_URL('https://misskey.io/@ai'),
[TEXT('[https://misskey.io/@ai')]
),
@@ -1136,12 +1176,13 @@ hoge`;
];
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,
+ 'plain',
N_URL('https://misskey.io/@ai'),
[
BOLD([
@@ -1159,18 +1200,19 @@ hoge`;
const input = '[@example](https://example.com)';
const output = [
LINK(
- false,
+ 'plain',
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,
+ 'plain',
N_URL('https://example.com'),
[
TEXT('@example'),
@@ -1188,7 +1230,7 @@ hoge`;
const input = '[foo](https://example.com/foo(bar))';
const output = [
LINK(
- false,
+ 'plain',
N_URL('https://example.com/foo(bar)'),
[TEXT('foo')]
),
@@ -1201,7 +1243,7 @@ hoge`;
const output = [
TEXT('('),
LINK(
- false,
+ 'plain',
N_URL('https://example.com/foo(bar)'),
[TEXT('foo')]
),
@@ -1215,7 +1257,7 @@ hoge`;
const output = [
TEXT('[test] foo '),
LINK(
- false,
+ 'plain',
N_URL('https://example.com'),
[TEXT('bar')]
),