1import path from 'path';
2import { visit } from 'unist-util-visit';
3import { URL } from 'url';
4
5/**
6 * @typedef {import('@types/mdast').Root} Root - https://github.com/syntax-tree/mdast#root
7 * @typedef {import('vfile').VFile} VFile - https://github.com/syntax-tree/unist#file
8 */
9
10const DEFAULT_OPTIONS = {
11  extension: 'mdx',
12  pagesDir: 'pages',
13  trailingSlash: true,
14};
15
16// This is a fallback domain, used to parse URLs. If origin matches this, its an internal link
17const FAKE_DOMAIN = 'https://fake.domain';
18
19/**
20 * This rewrites internal MDX links to absolute URLs from the root domain.
21 * It's the similar behavior of GitHub MDX linking, but for Next.
22 *
23 * @param {object} options
24 * @param {string} [options.extension="md"]
25 * @param {string} [options.pagesDir="pages"]
26 * @param {boolean} [options.trailingSlash=true]
27 * @returns {function} remark plugin
28 */
29export default function remarkLinkRewrite(options) {
30  const settings = { ...DEFAULT_OPTIONS, ...options };
31
32  /**
33   * @param {Root} tree
34   * @param {VFile} file
35   */
36  return (tree, file) => {
37    // we can't rewrite files without knowing where the file exists
38    if (!file.cwd || !file.history || !file.history.length) {
39      return;
40    }
41    // index references should be ignored, it's handled by Next
42    const ignoredIndex = 'index' + (settings.trailingSlash ? '/' : '');
43
44    visit(tree, 'link', node => {
45      // parse the url with a fallback fake domain, used to determine if it's internal or not
46      const ref = new URL(node.url, FAKE_DOMAIN);
47      // only rewrite internal non-url nodes
48      if (ref.origin === FAKE_DOMAIN) {
49        // if only a hash is provided, we need to calculate from the same file
50        const oldUrl =
51          ref.hash && ref.pathname === '/'
52            ? `${path.basename(file.history[0])}${ref.hash}`
53            : node.url;
54
55        // resolve the absolute path to the linked md file (supports hashes)
56        const absolute = path.resolve(path.dirname(file.history[0]), oldUrl);
57        // resolve the relative path between the linked file and our pages dir (root of the website)
58        const relative = path.relative(path.join(file.cwd, settings.pagesDir), absolute);
59        // rewrite the URL without the `.md` extension, using trailing slash or nothing
60        let newUrl = relative.replace(`.${settings.extension}`, settings.trailingSlash ? '/' : '');
61
62        // if the url is referencing the ignored index, remove it
63        if (newUrl.includes(ignoredIndex)) {
64          newUrl = newUrl.replace(ignoredIndex, '');
65        }
66
67        // force forward slash on non-posix systems
68        node.url = `/${newUrl.replace(/\\/g, '/')}`;
69      }
70    });
71  };
72}
73