summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/afraid-stingrays-sell.md5
-rw-r--r--packages/markdown/remark/src/index.ts9
-rw-r--r--packages/markdown/remark/src/rehype-jsx.ts46
-rw-r--r--packages/markdown/remark/test/autolinking.test.js96
4 files changed, 137 insertions, 19 deletions
diff --git a/.changeset/afraid-stingrays-sell.md b/.changeset/afraid-stingrays-sell.md
new file mode 100644
index 000000000..3c30c423b
--- /dev/null
+++ b/.changeset/afraid-stingrays-sell.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/markdown-remark': patch
+---
+
+Fix autolinking of URLs inside links
diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts
index 917976d5e..a11419474 100644
--- a/packages/markdown/remark/src/index.ts
+++ b/packages/markdown/remark/src/index.ts
@@ -99,14 +99,13 @@ export async function renderMarkdown(
parser
.use(isMDX ? [rehypeJsx, rehypeExpressions] : [rehypeRaw])
.use(rehypeEscape)
- .use(rehypeIslands);
+ .use(rehypeIslands)
+ .use([rehypeCollectHeaders])
+ .use(rehypeStringify, { allowDangerousHtml: true })
let result: string;
try {
- const vfile = await parser
- .use([rehypeCollectHeaders])
- .use(rehypeStringify, { allowDangerousHtml: true })
- .process(input);
+ const vfile = await parser.process(input);
result = vfile.toString();
} catch (err) {
// Ensure that the error message contains the input filename
diff --git a/packages/markdown/remark/src/rehype-jsx.ts b/packages/markdown/remark/src/rehype-jsx.ts
index daeb4d56a..06783ba85 100644
--- a/packages/markdown/remark/src/rehype-jsx.ts
+++ b/packages/markdown/remark/src/rehype-jsx.ts
@@ -1,15 +1,14 @@
+import type { RehypePlugin } from './types.js';
import { visit } from 'unist-util-visit';
const MDX_ELEMENTS = ['mdxJsxFlowElement', 'mdxJsxTextElement'];
-export default function rehypeJsx(): any {
- return function (node: any): any {
- visit(node, 'element', (child: any) => {
- child.tagName = `${child.tagName}`;
- });
- visit(node, MDX_ELEMENTS, (child: any, index: number | null, parent: any) => {
+
+export default function rehypeJsx(): ReturnType<RehypePlugin> {
+ return function (tree) {
+ visit(tree, MDX_ELEMENTS, (node: any, index: number | null, parent: any) => {
if (index === null || !Boolean(parent)) return;
- const attrs = child.attributes.reduce((acc: any[], entry: any) => {
+ const attrs = node.attributes.reduce((acc: any[], entry: any) => {
let attr = entry.value;
if (attr && typeof attr === 'object') {
attr = `{${attr.value}}`;
@@ -26,23 +25,42 @@ export default function rehypeJsx(): any {
return acc + ` ${entry.name}${attr ? '=' : ''}${attr}`;
}, '');
- if (child.children.length === 0) {
- child.type = 'raw';
- child.value = `<${child.name}${attrs} />`;
+ if (node.children.length === 0) {
+ node.type = 'raw';
+ node.value = `<${node.name}${attrs} />`;
return;
}
- // Replace the current child node with its children
+ // If the current node is a JSX <a> element, remove autolinks from its children
+ // to prevent Markdown code like `<a href="/">**Go to www.example.com now!**</a>`
+ // from creating a nested link to `www.example.com`
+ if (node.name === 'a') {
+ visit(node, 'element', (el, elIndex, elParent) => {
+ const isAutolink = (
+ el.tagName === 'a' &&
+ el.children.length === 1 &&
+ el.children[0].type === 'text' &&
+ el.children[0].value.match(/^(https?:\/\/|www\.)/i)
+ );
+
+ // If we found an autolink, remove it by replacing it with its text-only child
+ if (isAutolink) {
+ elParent.children.splice(elIndex, 1, el.children[0]);
+ }
+ });
+ }
+
+ // Replace the current node with its children
// wrapped by raw opening and closing tags
const openingTag = {
type: 'raw',
- value: `\n<${child.name}${attrs}>`,
+ value: `\n<${node.name}${attrs}>`,
};
const closingTag = {
type: 'raw',
- value: `</${child.name}>\n`,
+ value: `</${node.name}>\n`,
};
- parent.children.splice(index, 1, openingTag, ...child.children, closingTag);
+ parent.children.splice(index, 1, openingTag, ...node.children, closingTag);
});
};
}
diff --git a/packages/markdown/remark/test/autolinking.test.js b/packages/markdown/remark/test/autolinking.test.js
new file mode 100644
index 000000000..9224247e0
--- /dev/null
+++ b/packages/markdown/remark/test/autolinking.test.js
@@ -0,0 +1,96 @@
+import { renderMarkdown } from '../dist/index.js';
+import chai from 'chai';
+
+describe('autolinking', () => {
+ it('autolinks URLs starting with a protocol in plain text', async () => {
+ const { code } = await renderMarkdown(
+ `See https://example.com for more.`,
+ {}
+ );
+
+ chai
+ .expect(code.replace(/\n/g, ''))
+ .to.equal(`<p>See <a href="https://example.com">https://example.com</a> for more.</p>`);
+ });
+
+ it('autolinks URLs starting with "www." in plain text', async () => {
+ const { code } = await renderMarkdown(
+ `See www.example.com for more.`,
+ {}
+ );
+
+ chai
+ .expect(code.trim())
+ .to.equal(`<p>See <a href="http://www.example.com">www.example.com</a> for more.</p>`);
+ });
+
+ it('does not autolink URLs in code blocks', async () => {
+ const { code } = await renderMarkdown(
+ 'See `https://example.com` or `www.example.com` for more.',
+ {}
+ );
+
+ chai
+ .expect(code.trim())
+ .to.equal(`<p>See <code is:raw>https://example.com</code> or ` +
+ `<code is:raw>www.example.com</code> for more.</p>`);
+ });
+
+ it('does not autolink URLs in fenced code blocks', async () => {
+ const { code } = await renderMarkdown(
+ 'Example:\n```\nGo to https://example.com or www.example.com now.\n```',
+ {}
+ );
+
+ chai
+ .expect(code)
+ .to.contain(`<pre is:raw`)
+ .to.contain(`Go to https://example.com or www.example.com now.`);
+ });
+
+ it('does not autolink URLs starting with a protocol when nested inside links', async () => {
+ const { code } = await renderMarkdown(
+ `See [http://example.com](http://example.com) or ` +
+ `<a test href="https://example.com">https://example.com</a>`,
+ {}
+ );
+
+ chai
+ .expect(code.replace(/\n/g, ''))
+ .to.equal(
+ `<p>See <a href="http://example.com">http://example.com</a> or ` +
+ `<a test href="https://example.com">https://example.com</a></p>`
+ );
+ });
+
+ it('does not autolink URLs starting with "www." when nested inside links', async () => {
+ const { code } = await renderMarkdown(
+ `See [www.example.com](https://www.example.com) or ` +
+ `<a test href="https://www.example.com">www.example.com</a>`,
+ {}
+ );
+
+ chai
+ .expect(code.replace(/\n/g, ''))
+ .to.equal(
+ `<p>See <a href="https://www.example.com">www.example.com</a> or ` +
+ `<a test href="https://www.example.com">www.example.com</a></p>`
+ );
+ });
+
+ it('does not autolink URLs when nested several layers deep inside links', async () => {
+ const { code } = await renderMarkdown(
+ `<a href="https://www.example.com">**Visit _our www.example.com or ` +
+ `http://localhost pages_ for more!**</a>`,
+ {}
+ );
+
+ chai
+ .expect(code.replace(/\n/g, ''))
+ .to.equal(
+ `<a href="https://www.example.com"><strong>` +
+ `Visit <em>our www.example.com or http://localhost pages</em> for more!` +
+ `</strong></a>`
+ );
+ });
+});