Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const changelog =
const emptyPlugins = []
/** @type {Readonly<RemarkRehypeOptions>} */
const emptyRemarkRehypeOptions = {allowDangerousHtml: true}
const safeProtocol = /^(https?|ircs?|mailto|xmpp)$/i
const defaultSafeProtocol = /^(https?|ircs?|mailto|xmpp)$/i

// Mutable because we `delete` any time it’s used and a message is sent.
/** @type {ReadonlyArray<Readonly<Deprecation>>} */
Expand Down Expand Up @@ -317,7 +317,7 @@ function post(tree, options) {
const disallowedElements = options.disallowedElements
const skipHtml = options.skipHtml
const unwrapDisallowed = options.unwrapDisallowed
const urlTransform = options.urlTransform || defaultUrlTransform
const urlTransform = options.urlTransform || ((v) => defaultUrlTransform(v))

for (const deprecation of deprecations) {
if (Object.hasOwn(options, deprecation.from)) {
Expand Down Expand Up @@ -413,16 +413,19 @@ function post(tree, options) {
* Make a URL safe.
*
* This follows how GitHub works.
* It allows the protocols `http`, `https`, `irc`, `ircs`, `mailto`, and `xmpp`,
* and URLs relative to the current protocol (such as `/something`).
* By default it allows the protocols `http`, `https`, `irc`, `ircs`, `mailto`,
* and `xmpp`, and URLs relative to the current protocol (such as `/something`);
* pass `safeProtocol` to override which protocols are allowed.
*
* @satisfies {UrlTransform}
* @param {string} value
* URL.
* @param {RegExp} [safeProtocol]
* Regex matching protocols to allow (default: `/^(https?|ircs?|mailto|xmpp)$/i`);
* @returns {string}
* Safe URL.
*/
export function defaultUrlTransform(value) {
export function defaultUrlTransform(value, safeProtocol = defaultSafeProtocol) {
// Same as:
// <https://github.com/micromark/micromark/blob/929275e/packages/micromark-util-sanitize-uri/dev/index.js#L34>
// But without the `encode` part.
Expand Down
13 changes: 8 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ React component to render markdown.
* [`Markdown`](#markdown)
* [`MarkdownAsync`](#markdownasync)
* [`MarkdownHooks`](#markdownhooks)
* [`defaultUrlTransform(url)`](#defaulturltransformurl)
* [`defaultUrlTransform(url, safeProtocol)`](#defaulturltransformurl-safeprotocol)
* [`AllowElement`](#allowelement)
* [`Components`](#components)
* [`ExtraProps`](#extraprops)
Expand Down Expand Up @@ -238,18 +238,21 @@ see [`MarkdownAsync`][api-markdown-async].

React node (`ReactNode`).

### `defaultUrlTransform(url)`
### `defaultUrlTransform(url, safeProtocol)`

Make a URL safe.

This follows how GitHub works.
It allows the protocols `http`, `https`, `irc`, `ircs`, `mailto`, and `xmpp`,
and URLs relative to the current protocol (such as `/something`).
By default it allows the protocols `http`, `https`, `irc`, `ircs`, `mailto`,
and `xmpp`, and URLs relative to the current protocol (such as `/something`);
pass `safeProtocol` to override which protocols are allowed.

###### Parameters

* `url` (`string`)
— URL
* `safeProtocol` (`RegExp`, optional)
— regex matching protocols to allow (default: `/^(https?|ircs?|mailto|xmpp)$/i`)

###### Returns

Expand Down Expand Up @@ -829,7 +832,7 @@ abide by its terms.

[api-components]: #components

[api-default-url-transform]: #defaulturltransformurl
[api-default-url-transform]: #defaulturltransformurl-safeprotocol

[api-extra-props]: #extraprops

Expand Down
71 changes: 70 additions & 1 deletion test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import {render, waitFor} from '@testing-library/react'
import concatStream from 'concat-stream'
import {Component} from 'react'
import {renderToPipeableStream, renderToStaticMarkup} from 'react-dom/server'
import Markdown, {MarkdownAsync, MarkdownHooks} from 'react-markdown'
import Markdown, {
MarkdownAsync,
MarkdownHooks,
defaultUrlTransform
} from 'react-markdown'
import rehypeRaw from 'rehype-raw'
import rehypeStarryNight from 'rehype-starry-night'
import remarkGfm from 'remark-gfm'
Expand Down Expand Up @@ -414,6 +418,40 @@ test('Markdown', async function (t) {
)
})

await t.test(
'should support a custom `safeProtocol` via `urlTransform`',
function () {
assert.equal(
renderToStaticMarkup(
<Markdown
children="[a](tel:+123)"
urlTransform={function (url) {
return defaultUrlTransform(url, /^tel$/i)
}}
/>
),
'<p><a href="tel:+123">a</a></p>'
)
}
)

await t.test(
'should drop URLs whose protocol is not in `safeProtocol`',
function () {
assert.equal(
renderToStaticMarkup(
<Markdown
children="[a](https://b.com)"
urlTransform={function (url) {
return defaultUrlTransform(url, /^tel$/i)
}}
/>
),
'<p><a href="">a</a></p>'
)
}
)

await t.test('should support `skipHtml`', function () {
const actual = renderToStaticMarkup(
<Markdown children="a<i>b</i>c" skipHtml />
Expand Down Expand Up @@ -1058,6 +1096,37 @@ test('Markdown', async function (t) {
})
})

test('defaultUrlTransform', async function (t) {
await t.test('should allow default protocols', function () {
assert.equal(
defaultUrlTransform('https://example.com'),
'https://example.com'
)
assert.equal(defaultUrlTransform('mailto:a@b.c'), 'mailto:a@b.c')
})

await t.test('should drop unsafe protocols by default', function () {
assert.equal(defaultUrlTransform('data:text/html,<script>'), '')
})

await t.test('should allow relative URLs', function () {
assert.equal(defaultUrlTransform('/a/b'), '/a/b')
assert.equal(defaultUrlTransform('a?x:y'), 'a?x:y')
assert.equal(defaultUrlTransform('a#x:y'), 'a#x:y')
})

await t.test('should accept a custom `safeProtocol`', function () {
assert.equal(defaultUrlTransform('tel:+123', /^tel$/i), 'tel:+123')
})

await t.test(
'should drop protocols not matched by `safeProtocol`',
function () {
assert.equal(defaultUrlTransform('https://example.com', /^tel$/i), '')
}
)
})

test('MarkdownAsync', async function (t) {
await t.test('should support `MarkdownAsync` (1)', async function () {
assert.throws(function () {
Expand Down
Loading