Skip to content

Commit eca5e6b

Browse files
committed
Replace transformImageUri, transformLinkUri w/ urlTransform
1 parent ec2b134 commit eca5e6b

File tree

5 files changed

+56
-121
lines changed

5 files changed

+56
-121
lines changed

changelog.md

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes will be documented in this file.
44

55
## 9.0.0 - unreleased
66

7+
### Add `urlTransform`
8+
9+
The `transformImageUri` and `transformLinkUri` were removed.
10+
Having two functions is a bit much, particularly because there are more URLs
11+
you might want to change (or which might be unsafe so *we* make them safe).
12+
And their name and APIs were a bit weird.
13+
You can use the new `urlTransform` prop instead to change all your URLs.
14+
715
### Remove `includeElementIndex` option
816

917
The `includeElementIndex` option was removed, so `index` is never passed to

index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
* @typedef {import('./lib/index.js').Options} Options
55
*/
66

7-
export {Markdown as default, uriTransformer} from './lib/index.js'
7+
export {Markdown as default, defaultUrlTransform} from './lib/index.js'

lib/index.js

+30-57
Original file line numberDiff line numberDiff line change
@@ -59,44 +59,28 @@
5959
* Options to pass through to `remark-rehype`.
6060
* @property {boolean | null | undefined} [skipHtml=false]
6161
* Ignore HTML in markdown completely (default: `false`).
62-
* @property {TransformLink | false | null | undefined} [transformLinkUri]
63-
* Change URLs on images (default: `uriTransformer`);
64-
* pass `false` to allow all URLs, which is unsafe
65-
* @property {TransformImage | false | null | undefined} [transformImageUri]
66-
* Change URLs on links (default: `uriTransformer`);
67-
* pass `false` to allow all URLs, which is unsafe
6862
* @property {boolean | null | undefined} [unwrapDisallowed=false]
6963
* Extract (unwrap) the children of not allowed elements (default: `false`);
7064
* normally when say `strong` is disallowed, it and it’s children are dropped,
7165
* with `unwrapDisallowed` the element itself is replaced by its children.
66+
* @property {UrlTransform | null | undefined} [urlTransform]
67+
* Change URLs (default: `defaultUrlTransform`)
7268
*
73-
* @callback TransformImage
74-
* Transform URLs on images.
75-
* @param {string} src
69+
* @callback UrlTransform
70+
* Transform URLs.
71+
* @param {string} url
7672
* URL to transform.
77-
* @param {string} alt
78-
* Alt text.
79-
* @param {string | null} title
80-
* Title.
81-
* To do: pass `undefined`.
73+
* @param {string} key
74+
* Property name (example: `'href'`).
75+
* @param {Readonly<Element>} node
76+
* Node.
8277
* @returns {string | null | undefined}
8378
* Transformed URL (optional).
84-
*
85-
* @callback TransformLink
86-
* Transform URLs on links.
87-
* @param {string} href
88-
* URL to transform.
89-
* @param {ReadonlyArray<ElementContent>} children
90-
* Content.
91-
* @param {string | null} title
92-
* Title.
93-
* To do: pass `undefined`.
94-
* @returns {string}
95-
* Transformed URL (optional).
9679
*/
9780

9881
import {unreachable} from 'devlop'
9982
import {toJsxRuntime} from 'hast-util-to-jsx-runtime'
83+
import {urlAttributes} from 'html-url-attributes'
10084
// @ts-expect-error: untyped.
10185
import {Fragment, jsx, jsxs} from 'react/jsx-runtime'
10286
import remarkParse from 'remark-parse'
@@ -146,7 +130,9 @@ const deprecations = [
146130
{from: 'rawSourcePos', id: '#remove-rawsourcepos-option'},
147131
{from: 'renderers', id: 'change-renderers-to-components', to: 'components'},
148132
{from: 'source', id: 'change-source-to-children', to: 'children'},
149-
{from: 'sourcePos', id: '#remove-sourcepos-option'}
133+
{from: 'sourcePos', id: '#remove-sourcepos-option'},
134+
{from: 'transformImageUri', id: '#add-urltransform', to: 'urlTransform'},
135+
{from: 'transformLinkUri', id: '#add-urltransform', to: 'urlTransform'}
150136
]
151137

152138
/**
@@ -171,15 +157,8 @@ export function Markdown(options) {
171157
? {...options.remarkRehypeOptions, ...emptyRemarkRehypeOptions}
172158
: emptyRemarkRehypeOptions
173159
const skipHtml = options.skipHtml
174-
const transformImageUri =
175-
options.transformImageUri === undefined
176-
? uriTransformer
177-
: options.transformImageUri
178-
const transformLinkUri =
179-
options.transformLinkUri === undefined
180-
? uriTransformer
181-
: options.transformLinkUri
182160
const unwrapDisallowed = options.unwrapDisallowed
161+
const urlTransform = options.urlTransform || defaultUrlTransform
183162

184163
const processor = unified()
185164
.use(remarkParse)
@@ -265,26 +244,19 @@ export function Markdown(options) {
265244
return index
266245
}
267246

268-
if (transformLinkUri && node.type === 'element' && node.tagName === 'a') {
269-
node.properties.href = transformLinkUri(
270-
String(node.properties.href || ''),
271-
node.children,
272-
// To do: pass `undefined`.
273-
typeof node.properties.title === 'string' ? node.properties.title : null
274-
)
275-
}
276-
277-
if (
278-
transformImageUri &&
279-
node.type === 'element' &&
280-
node.tagName === 'img'
281-
) {
282-
node.properties.src = transformImageUri(
283-
String(node.properties.src || ''),
284-
String(node.properties.alt || ''),
285-
// To do: pass `undefined`.
286-
typeof node.properties.title === 'string' ? node.properties.title : null
287-
)
247+
if (node.type === 'element') {
248+
/** @type {string} */
249+
let key
250+
251+
for (key in urlAttributes) {
252+
if (own.call(urlAttributes, key) && own.call(node.properties, key)) {
253+
const value = node.properties[key]
254+
const test = urlAttributes[key]
255+
if (test === null || test.includes(node.tagName)) {
256+
node.properties[key] = urlTransform(String(value || ''), key, node)
257+
}
258+
}
259+
}
288260
}
289261

290262
if (node.type === 'element') {
@@ -316,13 +288,14 @@ export function Markdown(options) {
316288
/**
317289
* Make a URL safe.
318290
*
291+
* @satisfies {UrlTransform}
319292
* @param {string} value
320293
* URL.
321294
* @returns {string}
322295
* Safe URL.
323296
*/
324-
export function uriTransformer(value) {
325-
const url = (value || '').trim()
297+
export function defaultUrlTransform(value) {
298+
const url = value.trim()
326299
const first = url.charAt(0)
327300

328301
if (first === '#' || first === '/') {

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"@types/hast": "^3.0.0",
8181
"devlop": "^1.0.0",
8282
"hast-util-to-jsx-runtime": "^2.0.0",
83+
"html-url-attributes": "^3.0.0",
8384
"mdast-util-to-hast": "^13.0.0",
8485
"remark-parse": "^11.0.0",
8586
"remark-rehype": "^11.0.0",

test.jsx

+16-63
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ test('react-markdown', async function (t) {
1717
await t.test('should expose the public api', async function () {
1818
assert.deepEqual(Object.keys(await import('./index.js')).sort(), [
1919
'default',
20-
'uriTransformer'
20+
'defaultUrlTransform'
2121
])
2222
})
2323

@@ -345,15 +345,15 @@ test('react-markdown', async function (t) {
345345
)
346346
})
347347

348-
await t.test('should support `transformLinkUri`', function () {
348+
await t.test('should support `urlTransform` (`href` on `a`)', function () {
349349
assert.equal(
350350
asHtml(
351351
<Markdown
352352
children="[a](https://b.com 'c')"
353-
transformLinkUri={function (src, children, title) {
354-
assert.equal(src, 'https://b.com')
355-
assert.equal(children.length, 1)
356-
assert.equal(title, 'c')
353+
urlTransform={function (url, key, node) {
354+
assert.equal(url, 'https://b.com')
355+
assert.equal(key, 'href')
356+
assert.equal(node.tagName, 'a')
357357
return ''
358358
}}
359359
/>
@@ -362,15 +362,15 @@ test('react-markdown', async function (t) {
362362
)
363363
})
364364

365-
await t.test('should support `transformLinkUri` w/ empty URLs', function () {
365+
await t.test('should support `urlTransform` w/ empty URLs', function () {
366366
assert.equal(
367367
asHtml(
368368
<Markdown
369369
children="[]()"
370-
transformLinkUri={function (href, children, title) {
371-
assert.equal(href, '')
372-
assert.equal(children.length, 0)
373-
assert.equal(title, null)
370+
urlTransform={function (url, key, node) {
371+
assert.equal(url, '')
372+
assert.equal(key, 'href')
373+
assert.equal(node.tagName, 'a')
374374
return ''
375375
}}
376376
/>
@@ -379,30 +379,15 @@ test('react-markdown', async function (t) {
379379
)
380380
})
381381

382-
await t.test(
383-
'should support turning off `transformLinkUri` (dangerous)',
384-
function () {
385-
assert.equal(
386-
asHtml(
387-
<Markdown
388-
children="[](javascript:alert(1))"
389-
transformLinkUri={null}
390-
/>
391-
),
392-
'<p><a href="javascript:alert(1)"></a></p>'
393-
)
394-
}
395-
)
396-
397-
await t.test('should support `transformImageUri`', function () {
382+
await t.test('should support `urlTransform` (`src` on `img`)', function () {
398383
assert.equal(
399384
asHtml(
400385
<Markdown
401386
children="![a](https://b.com 'c')"
402-
transformImageUri={function (src, alt, title) {
403-
assert.equal(src, 'https://b.com')
404-
assert.equal(alt, 'a')
405-
assert.equal(title, 'c')
387+
urlTransform={function (url, key, node) {
388+
assert.equal(url, 'https://b.com')
389+
assert.equal(key, 'src')
390+
assert.equal(node.tagName, 'img')
406391
return ''
407392
}}
408393
/>
@@ -411,38 +396,6 @@ test('react-markdown', async function (t) {
411396
)
412397
})
413398

414-
await t.test('should support `transformImageUri` w/ empty URLs', function () {
415-
assert.equal(
416-
asHtml(
417-
<Markdown
418-
children="![]()"
419-
transformImageUri={function (href, alt, title) {
420-
assert.equal(href, '')
421-
assert.equal(alt, '')
422-
assert.equal(title, null)
423-
return ''
424-
}}
425-
/>
426-
),
427-
'<p><img src="" alt=""/></p>'
428-
)
429-
})
430-
431-
await t.test(
432-
'should support turning off `transformImageUri` (dangerous)',
433-
function () {
434-
assert.equal(
435-
asHtml(
436-
<Markdown
437-
children="![](javascript:alert(1))"
438-
transformImageUri={null}
439-
/>
440-
),
441-
'<p><img src="javascript:alert(1)" alt=""/></p>'
442-
)
443-
}
444-
)
445-
446399
await t.test('should support `skipHtml`', function () {
447400
const actual = asHtml(<Markdown children="a<i>b</i>c" skipHtml />)
448401
assert.equal(actual, '<p>abc</p>')

0 commit comments

Comments
 (0)