diff --git a/packages/docusaurus-mdx-loader/src/index.js b/packages/docusaurus-mdx-loader/src/index.js
index 5c3f9df26a..3081306ef1 100644
--- a/packages/docusaurus-mdx-loader/src/index.js
+++ b/packages/docusaurus-mdx-loader/src/index.js
@@ -14,6 +14,7 @@ const stringifyObject = require('stringify-object');
const slug = require('./remark/slug');
const rightToc = require('./remark/rightToc');
const transformImage = require('./remark/transformImage');
+const tranformAsset = require('./remark/transformAssets');
const DEFAULT_OPTIONS = {
rehypePlugins: [],
@@ -34,11 +35,16 @@ module.exports = async function (fileString) {
transformImage,
{staticDir: reqOptions.staticDir, filePath: this.resourcePath},
],
+ [
+ tranformAsset,
+ {staticDir: reqOptions.staticDir, filePath: this.resourcePath},
+ ],
...(reqOptions.remarkPlugins || []),
],
rehypePlugins: [
...(reqOptions.beforeDefaultRehypePlugins || []),
...DEFAULT_OPTIONS.rehypePlugins,
+
...(reqOptions.rehypePlugins || []),
],
filepath: this.resourcePath,
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/__snapshots__/index.test.js.snap b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/__snapshots__/index.test.js.snap
new file mode 100644
index 0000000000..4b2fe75f12
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/__snapshots__/index.test.js.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`transformAsset plugin fail if asset does not exist 1`] = `"Asset packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/doesNotExist.pdf used in packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/fail.md not found."`;
+
+exports[`transformAsset plugin fail if asset url is absent 1`] = `"Markdown link url is mandatory. filePath=packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/noUrl.md"`;
+
+exports[`transformAsset plugin pathname protocol 1`] = `
+"[asset](/asset/unchecked.pdf)
+"
+`;
+
+exports[`transformAsset plugin transform md links to 1`] = `
+"[asset](https://example.com/asset.pdf)
+
+
+
+asset
+
+[asset](asset.pdf \\"Title\\") 
+
+## Heading
+
+\`\`\`md
+[asset](./asset.pdf)
+\`\`\`
+
+assets
+
+[assets](/github/!file-loader!/assets.pdf)
+
+[asset](asset.pdf)
+"
+`;
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/asset.md b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/asset.md
new file mode 100644
index 0000000000..e346670120
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/asset.md
@@ -0,0 +1,19 @@
+[asset](https://example.com/asset.pdf)
+
+[](./asset.pdf)
+
+[asset](./asset.pdf)
+
+[asset](asset.pdf 'Title') 
+
+## Heading
+
+```md
+[asset](./asset.pdf)
+```
+
+[assets](!file-loader!./asset.pdf)
+
+[assets](/github/!file-loader!/assets.pdf)
+
+[asset](asset.pdf)
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/asset.pdf b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/asset.pdf
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/fail.md b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/fail.md
new file mode 100644
index 0000000000..cbbd9c3343
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/fail.md
@@ -0,0 +1 @@
+[asset](./doesNotExist.pdf)
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/noUrl.md b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/noUrl.md
new file mode 100644
index 0000000000..a35d39ef45
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/noUrl.md
@@ -0,0 +1 @@
+[asset]()
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/pathname.md b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/pathname.md
new file mode 100644
index 0000000000..6e20bcf3d3
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/fixtures/pathname.md
@@ -0,0 +1 @@
+[asset](pathname:///asset/unchecked.pdf)
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/index.test.js b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/index.test.js
new file mode 100644
index 0000000000..107f2c8e9d
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/transformAssets/__tests__/index.test.js
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {join} from 'path';
+import remark from 'remark';
+import mdx from 'remark-mdx';
+import vfile from 'to-vfile';
+import plugin from '..';
+import slug from '../../slug';
+
+const processFixture = async (name, options) => {
+ const path = join(__dirname, 'fixtures', `${name}.md`);
+ const file = await vfile.read(path);
+ const result = await remark()
+ .use(slug)
+ .use(mdx)
+ .use(plugin, {...options, filePath: path})
+ .process(file);
+
+ return result.toString();
+};
+
+describe('transformAsset plugin', () => {
+ test('fail if asset does not exist', async () => {
+ await expect(processFixture('fail')).rejects.toThrowErrorMatchingSnapshot();
+ });
+ test('fail if asset url is absent', async () => {
+ await expect(
+ processFixture('noUrl'),
+ ).rejects.toThrowErrorMatchingSnapshot();
+ });
+
+ test('transform md links to ', async () => {
+ const result = await processFixture('asset');
+ expect(result).toMatchSnapshot();
+ });
+
+ test('pathname protocol', async () => {
+ const result = await processFixture('pathname');
+ expect(result).toMatchSnapshot();
+ });
+});
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformAssets/index.js b/packages/docusaurus-mdx-loader/src/remark/transformAssets/index.js
new file mode 100644
index 0000000000..691a8b9c16
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/transformAssets/index.js
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+const visit = require('unist-util-visit');
+const path = require('path');
+const url = require('url');
+const fs = require('fs-extra');
+
+// Needed to throw errors with computer-agnostic path messages
+// Absolute paths are too dependant of user FS
+function toRelativePath(filePath) {
+ return path.relative(process.cwd(), filePath);
+}
+
+async function ensureAssetFileExist(assetPath, sourceFilePath) {
+ const assetExists = await fs.exists(assetPath);
+ if (!assetExists) {
+ throw new Error(
+ `Asset ${toRelativePath(assetPath)} used in ${toRelativePath(
+ sourceFilePath,
+ )} not found.`,
+ );
+ }
+}
+
+async function processLinkNode(node, index, parent, {filePath}) {
+ if (!node.url) {
+ throw new Error(
+ `Markdown link url is mandatory. filePath=${toRelativePath(filePath)}`,
+ );
+ }
+ const parsedUrl = url.parse(node.url);
+ const assetPath = node.url;
+ if (parsedUrl.protocol) {
+ // pathname:// is an escape hatch,
+ // in case user does not want his assets to be converted to require calls going through webpack loader
+ // we don't have to document this for now,
+ // it's mostly to make next release less risky (2.0.0-alpha.59)
+ if (parsedUrl.protocol === 'pathname:') {
+ node.url = node.url.replace('pathname://', '');
+ }
+ return;
+ }
+ if (
+ assetPath.match(/#|.md|.mdx/) ||
+ path.isAbsolute(assetPath) ||
+ !path.extname(assetPath) ||
+ !assetPath.startsWith('.')
+ ) {
+ if (!assetPath.startsWith('!')) {
+ return;
+ }
+ }
+
+ const expectedAssetPath = path.join(
+ path.dirname(filePath),
+ assetPath.replace(/!.*!/, ''),
+ );
+ await ensureAssetFileExist(expectedAssetPath, filePath);
+
+ node.type = 'jsx';
+ node.value = ``;
+ const {children} = node;
+ delete node.children;
+
+ parent.children.splice(index + 1, 0, {
+ type: 'paragraph',
+ children,
+ });
+
+ parent.children.splice(index + 2, 0, {type: 'jsx', value: ''});
+}
+
+const plugin = (options) => {
+ const transformer = async (root) => {
+ const promises = [];
+ visit(root, 'link', (node, index, parent) => {
+ promises.push(processLinkNode(node, index, parent, options));
+ });
+ await Promise.all(promises);
+ };
+ return transformer;
+};
+
+module.exports = plugin;
diff --git a/website/docs/markdown-features.mdx b/website/docs/markdown-features.mdx
index fc3ba4afe7..4945c46166 100644
--- a/website/docs/markdown-features.mdx
+++ b/website/docs/markdown-features.mdx
@@ -997,6 +997,10 @@ In the same way, you can link to existing assets by requiring them and using the
href={require('./assets/docusaurus-asset-example-pdf.pdf').default}>
Download this PDF !!!
+
+or
+
+[Download this PDF using Markdown !!!](./assets/docusaurus-asset-example-pdf.pdf)
```
+
+[Download this PDF using Markdown !!!](./assets/docusaurus-asset-example-pdf.pdf)
+
### Unknown assets
This require behavior is not supported for all file extensions, but as an escape hatch you can use the special Webpack syntax to force the `file-loader` to kick-in:
@@ -1017,6 +1024,10 @@ This require behavior is not supported for all file extensions, but as an escape
href={require('!file-loader!./assets/docusaurus-asset-example.xyz').default}>
Download this unknown file !!!
+
+or
+
+[Download this unknown file using Markdown](!file-loader!./assets/docusaurus-asset-example.xyz)
```
Download this unknown file !!!
+
+[Download this unknown file using Markdown !!!](!file-loader!./assets/docusaurus-asset-example.xyz)
+
+
+```md
+[](./assets/docusaurus-asset-example-pdf.pdf)
+```
+
+[](./assets/docusaurus-asset-example-pdf.pdf)
\ No newline at end of file