Skip to content

Commit

Permalink
fix(v2): linking to asset or external html page -> don't use history.…
Browse files Browse the repository at this point in the history
…push() (#3347)

* Rework markdown links to asset require processing + add test page

* implement pathname:// protocol / escape hatch at the Link level

* linking to assets: fix tests + avoid creating an useless nested paragraph

* fix assets linking doc

* attempt to fix windows e2e test

* try to fix windows errors
  • Loading branch information
slorber authored Aug 28, 2020
1 parent bd9b661 commit c7fc781
Show file tree
Hide file tree
Showing 21 changed files with 267 additions and 227 deletions.
6 changes: 3 additions & 3 deletions packages/docusaurus-mdx-loader/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ 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 transformLinks = require('./remark/transformLinks');

const DEFAULT_OPTIONS = {
rehypePlugins: [],
remarkPlugins: [emoji, slug, rightToc],
};

module.exports = async function (fileString) {
module.exports = async function docusaurusMdxLoader(fileString) {
const callback = this.async();

const {data, content} = matter(fileString);
Expand All @@ -36,7 +36,7 @@ module.exports = async function (fileString) {
{staticDir: reqOptions.staticDir, filePath: this.resourcePath},
],
[
tranformAsset,
transformLinks,
{staticDir: reqOptions.staticDir, filePath: this.resourcePath},
],
...(reqOptions.remarkPlugins || []),
Expand Down

This file was deleted.

This file was deleted.

91 changes: 0 additions & 91 deletions packages/docusaurus-mdx-loader/src/remark/transformAssets/index.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`transformAsset plugin fail if asset url is absent 1`] = `"Markdown link url is mandatory. filePath=packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/fixtures/noUrl.md, title=null"`;

exports[`transformAsset plugin pathname protocol 1`] = `
"[asset](pathname:///asset/unchecked.pdf)
"
`;

exports[`transformAsset plugin transform md links to <a /> 1`] = `
"[asset](https://example.com/asset.pdf)
<a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./asset.pdf').default} ></a>
<a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./asset.pdf').default} >asset</a>
<a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./asset.pdf').default} title={Title} >asset</a> ![seet](asset)
## Heading
\`\`\`md
[asset](./asset.pdf)
\`\`\`
[assets](!file-loader!./asset.pdf)
[assets](/github/!file-loader!/assets.pdf)
<a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./asset.pdf').default} >asset</a>
<a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./static/staticAsset.pdf').default} >staticAsset.pdf</a>
<a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./static/staticAsset.pdf').default} >@site/static/staticAsset.pdf</a>
"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@
[assets](/github/!file-loader!/assets.pdf)

[asset](asset.pdf)

[staticAsset.pdf](/staticAsset.pdf)

[@site/static/staticAsset.pdf](@site/static/staticAsset.pdf)
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,18 @@ import slug from '../../slug';

const processFixture = async (name, options) => {
const path = join(__dirname, 'fixtures', `${name}.md`);
const staticDir = join(__dirname, 'fixtures', 'static');
const file = await vfile.read(path);
const result = await remark()
.use(slug)
.use(mdx)
.use(plugin, {...options, filePath: path})
.use(plugin, {...options, filePath: path, staticDir})
.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'),
Expand Down
154 changes: 154 additions & 0 deletions packages/docusaurus-mdx-loader/src/remark/transformLinks/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* 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 {posixPath} = require('@docusaurus/utils');

const visit = require('unist-util-visit');
const path = require('path');
const url = require('url');
const fs = require('fs-extra');
const {getFileLoaderUtils} = require('@docusaurus/core/lib/webpack/utils');

const {
loaders: {inlineMarkdownLinkFileLoader},
} = getFileLoaderUtils();

// 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(fileSystemAssetPath, sourceFilePath) {
const assetExists = await fs.exists(fileSystemAssetPath);
if (!assetExists) {
throw new Error(
`Asset ${toRelativePath(fileSystemAssetPath)} used in ${toRelativePath(
sourceFilePath,
)} not found.`,
);
}
}

// transform the link node to a jsx link with a require() call
function toAssetRequireNode({node, index, parent, filePath, requireAssetPath}) {
let relativeRequireAssetPath = posixPath(
path.relative(path.dirname(filePath), requireAssetPath),
);

// nodejs does not like require("assets/file.pdf")
relativeRequireAssetPath = relativeRequireAssetPath.startsWith('.')
? relativeRequireAssetPath
: `./${relativeRequireAssetPath}`;

const hrefProp = `require('${inlineMarkdownLinkFileLoader}${relativeRequireAssetPath}').default`;

node.type = 'jsx';

node.value = `<a target="_blank" href={${hrefProp}} ${
node.title ? `title={${node.title}}` : ''
} >`;

const linkText = (node.children[0] && node.children[0].value) || '';
delete node.children;

parent.children.splice(index + 1, 0, {
type: 'text',
value: linkText,
});

parent.children.splice(index + 2, 0, {type: 'jsx', value: '</a>'});
}

// If the link looks like an asset link, we'll link to the asset,
// and use a require("assetUrl") (using webpack url-loader/file-loader)
// instead of navigating to such link
async function convertToAssetLinkIfNeeded({
node,
index,
parent,
staticDir,
filePath,
}) {
const assetPath = node.url;

const hasSiteAlias = assetPath.startsWith('@site/');
const hasAssetLikeExtension =
path.extname(assetPath) && !assetPath.match(/#|.md|.mdx|.html/);

const looksLikeAssetLink = hasSiteAlias || hasAssetLikeExtension;

if (!looksLikeAssetLink) {
return;
}

function toAssetLinkNode(requireAssetPath) {
toAssetRequireNode({
node,
index,
parent,
filePath,
requireAssetPath,
});
}

if (assetPath.startsWith('@site/')) {
const siteDir = path.join(staticDir, '..');
const fileSystemAssetPath = path.join(
siteDir,
assetPath.replace('@site/', ''),
);
await ensureAssetFileExist(fileSystemAssetPath, filePath);
toAssetLinkNode(fileSystemAssetPath);
} else if (path.isAbsolute(assetPath)) {
const fileSystemAssetPath = path.join(staticDir, assetPath);
if (await fs.exists(fileSystemAssetPath)) {
toAssetLinkNode(fileSystemAssetPath);
}
} else {
const fileSystemAssetPath = path.join(path.dirname(filePath), assetPath);
if (await fs.exists(fileSystemAssetPath)) {
toAssetLinkNode(fileSystemAssetPath);
}
}
}

async function processLinkNode({node, index, parent, filePath, staticDir}) {
if (!node.url) {
throw new Error(
`Markdown link url is mandatory. filePath=${toRelativePath(
filePath,
)}, title=${node.title}`,
);
}

const parsedUrl = url.parse(node.url);
if (parsedUrl.protocol) {
return;
}

await convertToAssetLinkIfNeeded({
node,
index,
parent,
staticDir,
filePath,
});
}

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;
Loading

0 comments on commit c7fc781

Please sign in to comment.