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\\") ![seet](asset) + +## 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') ![seet](asset) + +## 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-banner.png)](./assets/docusaurus-asset-example-pdf.pdf) +``` + +[![](./assets/docusaurus-asset-example-banner.png)](./assets/docusaurus-asset-example-pdf.pdf) \ No newline at end of file