From 2ebe04840d109d212af17813f8a3a158d54a531a Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 20 Sep 2019 18:29:27 +0800 Subject: [PATCH 1/7] Fix: data URL don't support query strings --- index.js | 20 ++++++++++---------- test.js | 45 +++++++++++++++++++++++++++++++-------------- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/index.js b/index.js index 20ab9de..1afc342 100644 --- a/index.js +++ b/index.js @@ -6,8 +6,8 @@ const testParameter = (name, filters) => { return filters.some(filter => filter instanceof RegExp ? filter.test(name) : filter === name); }; -const normalizeDataURL = urlString => { - const parts = urlString.trim().match(/^data:(.*?),(.*)$/); +const normalizeDataURL = (urlString, {stripHash}) => { + const parts = urlString.match(/^data:(.*?),(.*?)(?:#(.*))?$/); if (!parts) { throw new Error(`Invalid URL: ${urlString}`); @@ -15,6 +15,7 @@ const normalizeDataURL = urlString => { const mediaType = parts[1].split(';'); const body = parts[2]; + const hash = stripHash ? '' : parts[3]; let base64 = false; @@ -50,7 +51,7 @@ const normalizeDataURL = urlString => { normalizedMediaType.unshift(mimeType); } - return `data:${normalizedMediaType.join(';')},${base64 ? body.trim() : body}`; + return `data:${normalizedMediaType.join(';')},${base64 ? body.trim() : body}${hash ? `#${hash}` : ''}`; }; const normalizeUrl = (urlString, options) => { @@ -84,11 +85,16 @@ const normalizeUrl = (urlString, options) => { urlString = urlString.trim(); + // Data URL + if (/^data:/i.test(urlString)) { + return normalizeDataURL(urlString, options); + } + const hasRelativeProtocol = urlString.startsWith('//'); const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString); // Prepend protocol - if (!isRelativeUrl && !/^data:/i.test(urlString)) { + if (!isRelativeUrl) { urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol); } @@ -177,12 +183,6 @@ const normalizeUrl = (urlString, options) => { urlObj.searchParams.sort(); } - // Data URL - if (urlObj.protocol === 'data:') { - const url = normalizeDataURL(`${urlObj.protocol}${urlObj.pathname}`); - return `${url}${urlObj.search}${urlObj.hash}`; - } - if (options.removeTrailingSlash) { urlObj.pathname = urlObj.pathname.replace(/\/$/, ''); } diff --git a/test.js b/test.js index b65347f..d769505 100644 --- a/test.js +++ b/test.js @@ -225,25 +225,42 @@ test('data URL', t => { t.is(normalizeUrl('data:text/plain;charset=UTF-8,foo'), 'data:text/plain;charset=utf-8,foo'); // Remove spaces after the comma when it's base64. - t.is(normalizeUrl('data:image/gif;base64, R0lGODlhAQABAAAAACw= ?foo=bar'), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar'); + t.is(normalizeUrl('data:image/gif;base64, R0lGODlhAQABAAAAACw= #foo #bar'), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=#foo #bar'); // Keep spaces when it's not base64. - t.is(normalizeUrl('data:text/plain;charset=utf-8, foo ?foo=bar'), 'data:text/plain;charset=utf-8, foo?foo=bar'); + t.is(normalizeUrl('data:text/plain;charset=utf-8, foo #bar'), 'data:text/plain;charset=utf-8, foo #bar'); - // Data URL with query and hash. - t.is(normalizeUrl('data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar#baz'), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar#baz'); - - // Options. - t.is(normalizeUrl('data:text/plain;charset=utf-8,www.foo/index.html?foo=bar&a=a&utm_medium=test#baz', { + // Protocol should not be changed. + t.is(normalizeUrl('data:,', { defaultProtocol: 'http:', normalizeProtocol: true, forceHttp: true, - stripHash: true, - stripWWW: true, - stripProtocol: true, + stripProtocol: true + }), 'data:,'); + + // Option: removeTrailingSlash. + t.is(normalizeUrl('data:,foo/', { + removeTrailingSlash: true + }), 'data:,foo/'); + + // Option: removeDirectoryIndex. + t.is(normalizeUrl('data:,foo/index.html', { + removeTrailingSlash: true + }), 'data:,foo/index.html'); + + // Option: removeQueryParameters & sortQueryParameters. + t.is(normalizeUrl('data:,foo?foo=bar&a=a&utm_medium=test', { removeQueryParameters: [/^utm_\w+/i, 'ref'], - sortQueryParameters: true, - removeTrailingSlash: true, - removeDirectoryIndex: true - }), 'data:text/plain;charset=utf-8,www.foo/index.html?a=a&foo=bar'); + sortQueryParameters: true + }), 'data:,foo?foo=bar&a=a&utm_medium=test'); + + // Option: stripHash. + t.is(normalizeUrl('data:,foo#bar', { + stripHash: true + }), 'data:,foo'); + + // Option: stripWWW. + t.is(normalizeUrl('data:,www.foo.com', { + stripWWW: true + }), 'data:,www.foo.com'); }); From 967b8f2052ec35f8f06f97fb189bf439b0735d88 Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 20 Sep 2019 18:38:55 +0800 Subject: [PATCH 2/7] Strip empty hash test --- test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test.js b/test.js index d769505..64ed300 100644 --- a/test.js +++ b/test.js @@ -221,6 +221,9 @@ test('data URL', t => { // Lowercase the MIME type. t.is(normalizeUrl('data:TEXT/plain,foo'), 'data:text/plain,foo'); + // Strip empty hash. + t.is(normalizeUrl('data:,foo# '), 'data:,foo'); + // Lowercase the charset. t.is(normalizeUrl('data:text/plain;charset=UTF-8,foo'), 'data:text/plain;charset=utf-8,foo'); From db9c7b816a21ae6880337a6ada181a491fcec5cb Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 20 Sep 2019 18:47:14 +0800 Subject: [PATCH 3/7] Improve coverage --- index.js | 2 +- test.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 1afc342..4c99af1 100644 --- a/index.js +++ b/index.js @@ -36,7 +36,7 @@ const normalizeDataURL = (urlString, {stripHash}) => { value = value.toLowerCase(); } - return `${key}=${value}`; + return `${key}${value ? `=${value}` : ''}`; }); const normalizedMediaType = [ diff --git a/test.js b/test.js index 64ed300..e0ae30d 100644 --- a/test.js +++ b/test.js @@ -205,6 +205,12 @@ test('remove duplicate pathname slashes', t => { t.is(normalizeUrl('http://sindresorhus.com//foo'), 'http://sindresorhus.com/foo'); }); +test('Deprecated options', t => { + t.throws(() => normalizeUrl('', {normalizeHttps: true}), 'options.normalizeHttps is renamed to options.forceHttp'); + t.throws(() => normalizeUrl('', {normalizeHttp: true}), 'options.normalizeHttp is renamed to options.forceHttps'); + t.throws(() => normalizeUrl('', {stripFragment: true}), 'options.stripFragment is renamed to options.stripHash'); +}); + test('data URL', t => { // Invalid URL. t.throws(() => normalizeUrl('data:'), 'Invalid URL: data:'); @@ -224,6 +230,9 @@ test('data URL', t => { // Strip empty hash. t.is(normalizeUrl('data:,foo# '), 'data:,foo'); + // Key only mediaType attribute. + t.is(normalizeUrl('data:text/plain;foo=,'), 'data:text/plain;foo,'); + // Lowercase the charset. t.is(normalizeUrl('data:text/plain;charset=UTF-8,foo'), 'data:text/plain;charset=utf-8,foo'); From 0a84cca610e1d1349e1e1a99dc0c1b1b218bb71f Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 20 Sep 2019 18:56:11 +0800 Subject: [PATCH 4/7] cover line 32 test --- test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test.js b/test.js index e0ae30d..e70072c 100644 --- a/test.js +++ b/test.js @@ -228,6 +228,7 @@ test('data URL', t => { t.is(normalizeUrl('data:TEXT/plain,foo'), 'data:text/plain,foo'); // Strip empty hash. + t.is(normalizeUrl('data:,foo'), 'data:,foo'); t.is(normalizeUrl('data:,foo# '), 'data:,foo'); // Key only mediaType attribute. From 77a67a00931c328d1073bcb01247ca7ad4efa015 Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 20 Sep 2019 19:01:55 +0800 Subject: [PATCH 5/7] cover line 32 test --- test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.js b/test.js index e70072c..824647c 100644 --- a/test.js +++ b/test.js @@ -228,11 +228,11 @@ test('data URL', t => { t.is(normalizeUrl('data:TEXT/plain,foo'), 'data:text/plain,foo'); // Strip empty hash. - t.is(normalizeUrl('data:,foo'), 'data:,foo'); t.is(normalizeUrl('data:,foo# '), 'data:,foo'); // Key only mediaType attribute. t.is(normalizeUrl('data:text/plain;foo=,'), 'data:text/plain;foo,'); + t.is(normalizeUrl('data:text/plain; foo,'), 'data:text/plain;foo,'); // Lowercase the charset. t.is(normalizeUrl('data:text/plain;charset=UTF-8,foo'), 'data:text/plain;charset=utf-8,foo'); From 4db258e5a349dc783e22f44317ed0abea68008cc Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 20 Sep 2019 19:05:43 +0800 Subject: [PATCH 6/7] Lower case message --- test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.js b/test.js index 824647c..f9eb8f4 100644 --- a/test.js +++ b/test.js @@ -205,7 +205,7 @@ test('remove duplicate pathname slashes', t => { t.is(normalizeUrl('http://sindresorhus.com//foo'), 'http://sindresorhus.com/foo'); }); -test('Deprecated options', t => { +test('deprecated options', t => { t.throws(() => normalizeUrl('', {normalizeHttps: true}), 'options.normalizeHttps is renamed to options.forceHttp'); t.throws(() => normalizeUrl('', {normalizeHttp: true}), 'options.normalizeHttp is renamed to options.forceHttps'); t.throws(() => normalizeUrl('', {stripFragment: true}), 'options.stripFragment is renamed to options.stripHash'); From f01561e490572f7644e97f018b0cc0a41194ec7d Mon Sep 17 00:00:00 2001 From: fisker Date: Fri, 20 Sep 2019 22:25:46 +0800 Subject: [PATCH 7/7] use single option to test --- test.js | 43 ++++++++++++++----------------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/test.js b/test.js index f9eb8f4..0b42e81 100644 --- a/test.js +++ b/test.js @@ -243,37 +243,22 @@ test('data URL', t => { // Keep spaces when it's not base64. t.is(normalizeUrl('data:text/plain;charset=utf-8, foo #bar'), 'data:text/plain;charset=utf-8, foo #bar'); - // Protocol should not be changed. - t.is(normalizeUrl('data:,', { + // Options. + const options = { defaultProtocol: 'http:', normalizeProtocol: true, forceHttp: true, - stripProtocol: true - }), 'data:,'); - - // Option: removeTrailingSlash. - t.is(normalizeUrl('data:,foo/', { - removeTrailingSlash: true - }), 'data:,foo/'); - - // Option: removeDirectoryIndex. - t.is(normalizeUrl('data:,foo/index.html', { - removeTrailingSlash: true - }), 'data:,foo/index.html'); - - // Option: removeQueryParameters & sortQueryParameters. - t.is(normalizeUrl('data:,foo?foo=bar&a=a&utm_medium=test', { + stripHash: true, + stripWWW: true, + stripProtocol: true, removeQueryParameters: [/^utm_\w+/i, 'ref'], - sortQueryParameters: true - }), 'data:,foo?foo=bar&a=a&utm_medium=test'); - - // Option: stripHash. - t.is(normalizeUrl('data:,foo#bar', { - stripHash: true - }), 'data:,foo'); - - // Option: stripWWW. - t.is(normalizeUrl('data:,www.foo.com', { - stripWWW: true - }), 'data:,www.foo.com'); + sortQueryParameters: true, + removeTrailingSlash: true, + removeDirectoryIndex: true + }; + t.is(normalizeUrl('data:,sindresorhus.com/', options), 'data:,sindresorhus.com/'); + t.is(normalizeUrl('data:,sindresorhus.com/index.html', options), 'data:,sindresorhus.com/index.html'); + t.is(normalizeUrl('data:,sindresorhus.com?foo=bar&a=a&utm_medium=test', options), 'data:,sindresorhus.com?foo=bar&a=a&utm_medium=test'); + t.is(normalizeUrl('data:,foo#bar', options), 'data:,foo'); + t.is(normalizeUrl('data:,www.sindresorhus.com', options), 'data:,www.sindresorhus.com'); });