diff --git a/changelog.md b/changelog.md index 5fd55f18..ffa6ca75 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,14 @@ All notable changes will be documented in this file. ## 9.0.0 - unreleased +### Add `urlTransform` + +The `transformImageUri` and `transformLinkUri` were removed. +Having two functions is a bit much, particularly because there are more URLs +you might want to change (or which might be unsafe so *we* make them safe). +And their name and APIs were a bit weird. +You can use the new `urlTransform` prop instead to change all your URLs. + ### Remove `includeElementIndex` option The `includeElementIndex` option was removed, so `index` is never passed to diff --git a/index.js b/index.js index a2df3084..1c36187e 100644 --- a/index.js +++ b/index.js @@ -4,4 +4,4 @@ * @typedef {import('./lib/index.js').Options} Options */ -export {Markdown as default, uriTransformer} from './lib/index.js' +export {Markdown as default, defaultUrlTransform} from './lib/index.js' diff --git a/lib/index.js b/lib/index.js index 869526b0..b8edf44f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -59,44 +59,28 @@ * Options to pass through to `remark-rehype`. * @property {boolean | null | undefined} [skipHtml=false] * Ignore HTML in markdown completely (default: `false`). - * @property {TransformLink | false | null | undefined} [transformLinkUri] - * Change URLs on images (default: `uriTransformer`); - * pass `false` to allow all URLs, which is unsafe - * @property {TransformImage | false | null | undefined} [transformImageUri] - * Change URLs on links (default: `uriTransformer`); - * pass `false` to allow all URLs, which is unsafe * @property {boolean | null | undefined} [unwrapDisallowed=false] * Extract (unwrap) the children of not allowed elements (default: `false`); * normally when say `strong` is disallowed, it and it’s children are dropped, * with `unwrapDisallowed` the element itself is replaced by its children. + * @property {UrlTransform | null | undefined} [urlTransform] + * Change URLs (default: `defaultUrlTransform`) * - * @callback TransformImage - * Transform URLs on images. - * @param {string} src + * @callback UrlTransform + * Transform URLs. + * @param {string} url * URL to transform. - * @param {string} alt - * Alt text. - * @param {string | null} title - * Title. - * To do: pass `undefined`. + * @param {string} key + * Property name (example: `'href'`). + * @param {Readonly} node + * Node. * @returns {string | null | undefined} * Transformed URL (optional). - * - * @callback TransformLink - * Transform URLs on links. - * @param {string} href - * URL to transform. - * @param {ReadonlyArray} children - * Content. - * @param {string | null} title - * Title. - * To do: pass `undefined`. - * @returns {string} - * Transformed URL (optional). */ import {unreachable} from 'devlop' import {toJsxRuntime} from 'hast-util-to-jsx-runtime' +import {urlAttributes} from 'html-url-attributes' // @ts-expect-error: untyped. import {Fragment, jsx, jsxs} from 'react/jsx-runtime' import remarkParse from 'remark-parse' @@ -146,7 +130,9 @@ const deprecations = [ {from: 'rawSourcePos', id: '#remove-rawsourcepos-option'}, {from: 'renderers', id: 'change-renderers-to-components', to: 'components'}, {from: 'source', id: 'change-source-to-children', to: 'children'}, - {from: 'sourcePos', id: '#remove-sourcepos-option'} + {from: 'sourcePos', id: '#remove-sourcepos-option'}, + {from: 'transformImageUri', id: '#add-urltransform', to: 'urlTransform'}, + {from: 'transformLinkUri', id: '#add-urltransform', to: 'urlTransform'} ] /** @@ -171,15 +157,8 @@ export function Markdown(options) { ? {...options.remarkRehypeOptions, ...emptyRemarkRehypeOptions} : emptyRemarkRehypeOptions const skipHtml = options.skipHtml - const transformImageUri = - options.transformImageUri === undefined - ? uriTransformer - : options.transformImageUri - const transformLinkUri = - options.transformLinkUri === undefined - ? uriTransformer - : options.transformLinkUri const unwrapDisallowed = options.unwrapDisallowed + const urlTransform = options.urlTransform || defaultUrlTransform const processor = unified() .use(remarkParse) @@ -265,26 +244,19 @@ export function Markdown(options) { return index } - if (transformLinkUri && node.type === 'element' && node.tagName === 'a') { - node.properties.href = transformLinkUri( - String(node.properties.href || ''), - node.children, - // To do: pass `undefined`. - typeof node.properties.title === 'string' ? node.properties.title : null - ) - } - - if ( - transformImageUri && - node.type === 'element' && - node.tagName === 'img' - ) { - node.properties.src = transformImageUri( - String(node.properties.src || ''), - String(node.properties.alt || ''), - // To do: pass `undefined`. - typeof node.properties.title === 'string' ? node.properties.title : null - ) + if (node.type === 'element') { + /** @type {string} */ + let key + + for (key in urlAttributes) { + if (own.call(urlAttributes, key) && own.call(node.properties, key)) { + const value = node.properties[key] + const test = urlAttributes[key] + if (test === null || test.includes(node.tagName)) { + node.properties[key] = urlTransform(String(value || ''), key, node) + } + } + } } if (node.type === 'element') { @@ -316,13 +288,14 @@ export function Markdown(options) { /** * Make a URL safe. * + * @satisfies {UrlTransform} * @param {string} value * URL. * @returns {string} * Safe URL. */ -export function uriTransformer(value) { - const url = (value || '').trim() +export function defaultUrlTransform(value) { + const url = value.trim() const first = url.charAt(0) if (first === '#' || first === '/') { diff --git a/package.json b/package.json index 42b8afde..804d2e94 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "@types/hast": "^3.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", diff --git a/test.jsx b/test.jsx index 83fe93f7..120bf26a 100644 --- a/test.jsx +++ b/test.jsx @@ -17,7 +17,7 @@ test('react-markdown', async function (t) { await t.test('should expose the public api', async function () { assert.deepEqual(Object.keys(await import('./index.js')).sort(), [ 'default', - 'uriTransformer' + 'defaultUrlTransform' ]) }) @@ -345,15 +345,15 @@ test('react-markdown', async function (t) { ) }) - await t.test('should support `transformLinkUri`', function () { + await t.test('should support `urlTransform` (`href` on `a`)', function () { assert.equal( asHtml( @@ -362,15 +362,15 @@ test('react-markdown', async function (t) { ) }) - await t.test('should support `transformLinkUri` w/ empty URLs', function () { + await t.test('should support `urlTransform` w/ empty URLs', function () { assert.equal( asHtml( @@ -379,30 +379,15 @@ test('react-markdown', async function (t) { ) }) - await t.test( - 'should support turning off `transformLinkUri` (dangerous)', - function () { - assert.equal( - asHtml( - - ), - '

' - ) - } - ) - - await t.test('should support `transformImageUri`', function () { + await t.test('should support `urlTransform` (`src` on `img`)', function () { assert.equal( asHtml( @@ -411,38 +396,6 @@ test('react-markdown', async function (t) { ) }) - await t.test('should support `transformImageUri` w/ empty URLs', function () { - assert.equal( - asHtml( - - ), - '

' - ) - }) - - await t.test( - 'should support turning off `transformImageUri` (dangerous)', - function () { - assert.equal( - asHtml( - - ), - '

' - ) - } - ) - await t.test('should support `skipHtml`', function () { const actual = asHtml() assert.equal(actual, '

abc

')