From cf2b896e4e1491397aa1a41e8592c1de0ba40353 Mon Sep 17 00:00:00 2001 From: fisker Date: Sun, 7 Jul 2019 12:42:13 +0800 Subject: [PATCH 01/17] Add dataURLs support --- index.js | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- test.js | 19 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index ac386a0..39ce543 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,50 @@ const testParameter = (name, filters) => { return filters.some(filter => filter instanceof RegExp ? filter.test(name) : filter === name); }; +// from https://github.com/killmenot/valid-data-url/blob/8d56cab866469a4608001f0e8c5d73f65789ba13/index.js#L24 +const dataURLRegex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z0-9-.!#$%*+.{}|~`]+=[a-z0-9-.!#$%*+.{}|~`]+)*)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*?)$/i; + +const isValidDataURL = urlString => { + return dataURLRegex.test(urlString) +} + +const parseDataURL = urlString => { + if (!isValidDataURL(urlString)) { + throw new Error(`Invalid URL: ${urlString}`); + } + + const parts = urlString.trim().match(dataURLRegex); + const parsed = {}; + + if (parts[1]) { + const mimeType = parts[1].toLowerCase(); + const [contentType, ...attributes] = mimeType.split(';'); + // TODO: use `Object.fromEntries` + // Object.assign(parsed, Object.fromEntries(attributes.map(attribute => attribute.split('=')))) + attributes.reduce((parsed, attribute) => { + const [key, value] = attribute.split('=') + parsed[key] = value + return parsed; + }, parsed); + + parsed.mimeType = mimeType; + parsed.contentType = contentType; + } + + parsed.base64 = !!parts[parts.length - 2]; + parsed.body = parts[parts.length - 1] || ''; + + return parsed; +}; + +const normalizeDataURL = urlString => { + const data = parseDataURL(urlString); + + const body = data.base64 ? data.body.trim() : data.body + + return `data:${data.mimeType}${data.base64 ? ';base64' : ''},${body}` +} + const normalizeUrl = (urlString, options) => { options = { defaultProtocol: 'http:', @@ -41,7 +85,7 @@ const normalizeUrl = (urlString, options) => { const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString); // Prepend protocol - if (!isRelativeUrl) { + if (!isRelativeUrl && !/^data?:/i.test(urlString)) { urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol); } @@ -152,6 +196,11 @@ const normalizeUrl = (urlString, options) => { urlString = urlString.replace(/^(?:https?:)?\/\//, ''); } + if (urlObj.protocol === 'data:') { + const url = normalizeDataURL(`${urlObj.protocol}${urlObj.pathname}`) + return `${url}${urlObj.search}${urlObj.hash}` + } + return urlString; }; diff --git a/test.js b/test.js index 5fbc8b4..8e01565 100644 --- a/test.js +++ b/test.js @@ -204,3 +204,22 @@ test('remove duplicate pathname slashes', t => { t.is(normalizeUrl('http://sindresorhus.com:5000//foo'), 'http://sindresorhus.com:5000/foo'); t.is(normalizeUrl('http://sindresorhus.com//foo'), 'http://sindresorhus.com/foo'); }); + +test('DataURLs', t => { + t.throws(() => { + normalizeUrl('data:text/plain;charset=UTF-8;,foo'); + }, 'Invalid URL: data:text/plain;charset=UTF-8;,foo'); + // Lowercase the mimeType + 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'); + // 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'); + // with query and hash + t.is(normalizeUrl('data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar#baz'), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar#baz'); + // removeQueryParameters & stripHash + t.is(normalizeUrl('data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar&utm_medium=test#baz', { + removeQueryParameters: [/^utm_\w+/i, 'ref'], + stripHash: true + }), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar'); +}) From b9d9b99a66678099453ffc659c6e2372fe7ed81d Mon Sep 17 00:00:00 2001 From: fisker Date: Sun, 7 Jul 2019 12:53:38 +0800 Subject: [PATCH 02/17] style: fix code style --- index.js | 39 ++++++++++++++++++++------------------- test.js | 6 +++--- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index 39ce543..5882826 100644 --- a/index.js +++ b/index.js @@ -6,49 +6,50 @@ const testParameter = (name, filters) => { return filters.some(filter => filter instanceof RegExp ? filter.test(name) : filter === name); }; -// from https://github.com/killmenot/valid-data-url/blob/8d56cab866469a4608001f0e8c5d73f65789ba13/index.js#L24 +// From https://github.com/killmenot/valid-data-url/blob/8d56cab866469a4608001f0e8c5d73f65789ba13/index.js#L24 +// eslint-disable-next-line no-useless-escape const dataURLRegex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z0-9-.!#$%*+.{}|~`]+=[a-z0-9-.!#$%*+.{}|~`]+)*)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*?)$/i; const isValidDataURL = urlString => { - return dataURLRegex.test(urlString) -} + return dataURLRegex.test(urlString); +}; const parseDataURL = urlString => { if (!isValidDataURL(urlString)) { throw new Error(`Invalid URL: ${urlString}`); } - const parts = urlString.trim().match(dataURLRegex); - const parsed = {}; + const parts = urlString.trim().match(dataURLRegex); + const parsed = {}; - if (parts[1]) { + if (parts[1]) { const mimeType = parts[1].toLowerCase(); - const [contentType, ...attributes] = mimeType.split(';'); + const [contentType, ...attributes] = mimeType.split(';'); // TODO: use `Object.fromEntries` // Object.assign(parsed, Object.fromEntries(attributes.map(attribute => attribute.split('=')))) attributes.reduce((parsed, attribute) => { - const [key, value] = attribute.split('=') - parsed[key] = value + const [key, value] = attribute.split('='); + parsed[key] = value; return parsed; }, parsed); parsed.mimeType = mimeType; - parsed.contentType = contentType; - } + parsed.contentType = contentType; + } - parsed.base64 = !!parts[parts.length - 2]; - parsed.body = parts[parts.length - 1] || ''; + parsed.base64 = Boolean(parts[parts.length - 2]); + parsed.body = parts[parts.length - 1] || ''; - return parsed; + return parsed; }; const normalizeDataURL = urlString => { const data = parseDataURL(urlString); - const body = data.base64 ? data.body.trim() : data.body + const body = data.base64 ? data.body.trim() : data.body; - return `data:${data.mimeType}${data.base64 ? ';base64' : ''},${body}` -} + return `data:${data.mimeType}${data.base64 ? ';base64' : ''},${body}`; +}; const normalizeUrl = (urlString, options) => { options = { @@ -197,8 +198,8 @@ const normalizeUrl = (urlString, options) => { } if (urlObj.protocol === 'data:') { - const url = normalizeDataURL(`${urlObj.protocol}${urlObj.pathname}`) - return `${url}${urlObj.search}${urlObj.hash}` + const url = normalizeDataURL(`${urlObj.protocol}${urlObj.pathname}`); + return `${url}${urlObj.search}${urlObj.hash}`; } return urlString; diff --git a/test.js b/test.js index 8e01565..f8efe97 100644 --- a/test.js +++ b/test.js @@ -215,11 +215,11 @@ test('DataURLs', t => { 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'); - // with query and hash + // DataURL with query and hash t.is(normalizeUrl('data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar#baz'), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar#baz'); - // removeQueryParameters & stripHash + // Options: removeQueryParameters & stripHash t.is(normalizeUrl('data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar&utm_medium=test#baz', { removeQueryParameters: [/^utm_\w+/i, 'ref'], stripHash: true }), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar'); -}) +}); From bcd7157f1e2ba281fb1bc6db7aa9ca3ddce228e8 Mon Sep 17 00:00:00 2001 From: fisker Date: Sun, 7 Jul 2019 13:03:10 +0800 Subject: [PATCH 03/17] more options test --- index.js | 11 ++++++----- test.js | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 5882826..04574a3 100644 --- a/index.js +++ b/index.js @@ -175,6 +175,12 @@ const normalizeUrl = (urlString, options) => { urlObj.searchParams.sort(); } + // DataUrls + 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(/\/$/, ''); } @@ -197,11 +203,6 @@ const normalizeUrl = (urlString, options) => { urlString = urlString.replace(/^(?:https?:)?\/\//, ''); } - if (urlObj.protocol === 'data:') { - const url = normalizeDataURL(`${urlObj.protocol}${urlObj.pathname}`); - return `${url}${urlObj.search}${urlObj.hash}`; - } - return urlString; }; diff --git a/test.js b/test.js index f8efe97..6e09854 100644 --- a/test.js +++ b/test.js @@ -217,9 +217,17 @@ test('DataURLs', t => { t.is(normalizeUrl('data:text/plain;charset=utf-8, foo ?foo=bar'), 'data:text/plain;charset=utf-8, foo?foo=bar'); // DataURL with query and hash t.is(normalizeUrl('data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar#baz'), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar#baz'); - // Options: removeQueryParameters & stripHash - t.is(normalizeUrl('data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar&utm_medium=test#baz', { + // Options + t.is(normalizeUrl('data:text/plain;charset=utf-8,www.foo/index.html?foo=bar&a=a&utm_medium=test#baz', { + defaultProtocol: 'http:', + normalizeProtocol: true, + forceHttp: true, + stripHash: true, + stripWWW: true, + stripProtocol: true, removeQueryParameters: [/^utm_\w+/i, 'ref'], - stripHash: true - }), 'data:image/gif;base64,R0lGODlhAQABAAAAACw=?foo=bar'); + sortQueryParameters: true, + removeTrailingSlash: true, + removeDirectoryIndex: true + }), 'data:text/plain;charset=utf-8,www.foo/index.html?a=a&foo=bar'); }); From 2585bb2356d5235aa3576a60eb816efe516cf44a Mon Sep 17 00:00:00 2001 From: fisker Date: Sun, 7 Jul 2019 13:12:11 +0800 Subject: [PATCH 04/17] test: cover empty mimeType --- index.js | 3 ++- test.js | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 04574a3..3eae79c 100644 --- a/index.js +++ b/index.js @@ -47,8 +47,9 @@ const normalizeDataURL = urlString => { const data = parseDataURL(urlString); const body = data.base64 ? data.body.trim() : data.body; + const {mimeType = ''} = data - return `data:${data.mimeType}${data.base64 ? ';base64' : ''},${body}`; + return `data:${mimeType}${data.base64 ? ';base64' : ''},${body}`; }; const normalizeUrl = (urlString, options) => { diff --git a/test.js b/test.js index 6e09854..c90f1c9 100644 --- a/test.js +++ b/test.js @@ -209,6 +209,8 @@ test('DataURLs', t => { t.throws(() => { normalizeUrl('data:text/plain;charset=UTF-8;,foo'); }, 'Invalid URL: data:text/plain;charset=UTF-8;,foo'); + // Empty mimeType + t.is(normalizeUrl('data:,'), 'data:,'); // Lowercase the mimeType 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 From b637a57f1136df3cccb1a513b6e36ab9bd3784f3 Mon Sep 17 00:00:00 2001 From: fisker Date: Sun, 7 Jul 2019 13:14:27 +0800 Subject: [PATCH 05/17] code style --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 3eae79c..3bdcf2f 100644 --- a/index.js +++ b/index.js @@ -47,7 +47,7 @@ const normalizeDataURL = urlString => { const data = parseDataURL(urlString); const body = data.base64 ? data.body.trim() : data.body; - const {mimeType = ''} = data + const {mimeType = ''} = data; return `data:${mimeType}${data.base64 ? ';base64' : ''},${body}`; }; From c5f933346e6be13be2eaf33fae80c9b3ab36e0ce Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Tue, 17 Sep 2019 12:59:23 +0700 Subject: [PATCH 06/17] Update test.js --- test.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/test.js b/test.js index c90f1c9..859867f 100644 --- a/test.js +++ b/test.js @@ -205,21 +205,27 @@ test('remove duplicate pathname slashes', t => { t.is(normalizeUrl('http://sindresorhus.com//foo'), 'http://sindresorhus.com/foo'); }); -test('DataURLs', t => { +test('data URL', t => { t.throws(() => { normalizeUrl('data:text/plain;charset=UTF-8;,foo'); }, 'Invalid URL: data:text/plain;charset=UTF-8;,foo'); - // Empty mimeType + + // Empty MIME type. t.is(normalizeUrl('data:,'), 'data:,'); - // Lowercase the mimeType + + // Lowercase the MIME type. 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 + + // 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'); - // Keep spaces when it's not base64 + + // 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'); - // DataURL with query and hash + + // 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 + + // Options. t.is(normalizeUrl('data:text/plain;charset=utf-8,www.foo/index.html?foo=bar&a=a&utm_medium=test#baz', { defaultProtocol: 'http:', normalizeProtocol: true, From 4af7cc426030fd1bc559e66d4e334ab954f48db7 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Tue, 17 Sep 2019 13:01:07 +0700 Subject: [PATCH 07/17] Update index.js --- index.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 3bdcf2f..d4b896a 100644 --- a/index.js +++ b/index.js @@ -10,9 +10,7 @@ const testParameter = (name, filters) => { // eslint-disable-next-line no-useless-escape const dataURLRegex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z0-9-.!#$%*+.{}|~`]+=[a-z0-9-.!#$%*+.{}|~`]+)*)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*?)$/i; -const isValidDataURL = urlString => { - return dataURLRegex.test(urlString); -}; +const isValidDataURL = urlString => dataURLRegex.test(urlString); const parseDataURL = urlString => { if (!isValidDataURL(urlString)) { @@ -25,7 +23,7 @@ const parseDataURL = urlString => { if (parts[1]) { const mimeType = parts[1].toLowerCase(); const [contentType, ...attributes] = mimeType.split(';'); - // TODO: use `Object.fromEntries` + // TODO: Use `Object.fromEntries` when targeting Node.js 10 // Object.assign(parsed, Object.fromEntries(attributes.map(attribute => attribute.split('=')))) attributes.reduce((parsed, attribute) => { const [key, value] = attribute.split('='); @@ -45,10 +43,8 @@ const parseDataURL = urlString => { const normalizeDataURL = urlString => { const data = parseDataURL(urlString); - const body = data.base64 ? data.body.trim() : data.body; const {mimeType = ''} = data; - return `data:${mimeType}${data.base64 ? ';base64' : ''},${body}`; }; From 128d9552e768fb9359b7d41dbb5200f64b4d7afa Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Tue, 17 Sep 2019 13:01:48 +0700 Subject: [PATCH 08/17] Update index.js --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index d4b896a..f512467 100644 --- a/index.js +++ b/index.js @@ -172,7 +172,7 @@ const normalizeUrl = (urlString, options) => { urlObj.searchParams.sort(); } - // DataUrls + // Data URL if (urlObj.protocol === 'data:') { const url = normalizeDataURL(`${urlObj.protocol}${urlObj.pathname}`); return `${url}${urlObj.search}${urlObj.hash}`; From 62ed83dadf2ade08a71a879b4531f6345cc43c55 Mon Sep 17 00:00:00 2001 From: fisker Date: Wed, 18 Sep 2019 09:00:45 +0800 Subject: [PATCH 09/17] fix data protocol regexp --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index f512467..35c2285 100644 --- a/index.js +++ b/index.js @@ -83,7 +83,7 @@ const normalizeUrl = (urlString, options) => { const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString); // Prepend protocol - if (!isRelativeUrl && !/^data?:/i.test(urlString)) { + if (!isRelativeUrl && !/^data:/i.test(urlString)) { urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol); } From f4bcdb6abf80808df09a6d426b8f0f9d5c580f58 Mon Sep 17 00:00:00 2001 From: fisker Date: Wed, 18 Sep 2019 09:06:27 +0800 Subject: [PATCH 10/17] Fix eslint error --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 35c2285..4441090 100644 --- a/index.js +++ b/index.js @@ -6,9 +6,9 @@ const testParameter = (name, filters) => { return filters.some(filter => filter instanceof RegExp ? filter.test(name) : filter === name); }; -// From https://github.com/killmenot/valid-data-url/blob/8d56cab866469a4608001f0e8c5d73f65789ba13/index.js#L24 -// eslint-disable-next-line no-useless-escape -const dataURLRegex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z0-9-.!#$%*+.{}|~`]+=[a-z0-9-.!#$%*+.{}|~`]+)*)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*?)$/i; +// Based on valid-data-url +// https://github.com/killmenot/valid-data-url/blob/8d56cab866469a4608001f0e8c5d73f65789ba13/index.js#L24 +const dataURLRegex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z0-9-.!#$%*+.{}|~`]+=[a-z0-9-.!#$%*+.{}|~`]+)*)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*?)$/i; const isValidDataURL = urlString => dataURLRegex.test(urlString); From 6ecd048e63d21909aac65e148870e374e13dec99 Mon Sep 17 00:00:00 2001 From: fisker Date: Wed, 18 Sep 2019 09:21:44 +0800 Subject: [PATCH 11/17] Use `for...of` instead of `reduce` --- index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 4441090..ddb6014 100644 --- a/index.js +++ b/index.js @@ -25,11 +25,10 @@ const parseDataURL = urlString => { const [contentType, ...attributes] = mimeType.split(';'); // TODO: Use `Object.fromEntries` when targeting Node.js 10 // Object.assign(parsed, Object.fromEntries(attributes.map(attribute => attribute.split('=')))) - attributes.reduce((parsed, attribute) => { + for (const attribute of attributes) { const [key, value] = attribute.split('='); parsed[key] = value; - return parsed; - }, parsed); + } parsed.mimeType = mimeType; parsed.contentType = contentType; From ea704575c58d8b39dff421aca7501d7d4a502377 Mon Sep 17 00:00:00 2001 From: fisker Date: Wed, 18 Sep 2019 10:56:05 +0800 Subject: [PATCH 12/17] Refactor normalizeDataURL --- index.js | 61 +++++++++++++++++++++++++++++--------------------------- test.js | 16 +++++++++++---- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/index.js b/index.js index ddb6014..e769356 100644 --- a/index.js +++ b/index.js @@ -6,45 +6,48 @@ const testParameter = (name, filters) => { return filters.some(filter => filter instanceof RegExp ? filter.test(name) : filter === name); }; -// Based on valid-data-url -// https://github.com/killmenot/valid-data-url/blob/8d56cab866469a4608001f0e8c5d73f65789ba13/index.js#L24 -const dataURLRegex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z0-9-.!#$%*+.{}|~`]+=[a-z0-9-.!#$%*+.{}|~`]+)*)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*?)$/i; - -const isValidDataURL = urlString => dataURLRegex.test(urlString); +const normalizeDataURL = urlString => { + const parts = urlString.trim().match(/^data:(.*?),(.*)$/); -const parseDataURL = urlString => { - if (!isValidDataURL(urlString)) { + if (!parts) { throw new Error(`Invalid URL: ${urlString}`); } - const parts = urlString.trim().match(dataURLRegex); - const parsed = {}; + const mediaType = parts[1].split(';'); + const body = parts[2]; - if (parts[1]) { - const mimeType = parts[1].toLowerCase(); - const [contentType, ...attributes] = mimeType.split(';'); - // TODO: Use `Object.fromEntries` when targeting Node.js 10 - // Object.assign(parsed, Object.fromEntries(attributes.map(attribute => attribute.split('=')))) - for (const attribute of attributes) { - const [key, value] = attribute.split('='); - parsed[key] = value; - } + let base64 = false; - parsed.mimeType = mimeType; - parsed.contentType = contentType; + if (mediaType[mediaType.length - 1] === 'base64') { + mediaType.pop(); + base64 = true; } - parsed.base64 = Boolean(parts[parts.length - 2]); - parsed.body = parts[parts.length - 1] || ''; + // Lowercase MIME type + const mimeType = (mediaType.shift() || '').toLowerCase(); + const attributes = mediaType + .filter(Boolean) + .map(attribute => { + let [key, value] = attribute.split('=').map(string => string.trim()); - return parsed; -}; + // Lowercase `charset` + if (key === 'charset') { + value = value.toLowerCase(); + } -const normalizeDataURL = urlString => { - const data = parseDataURL(urlString); - const body = data.base64 ? data.body.trim() : data.body; - const {mimeType = ''} = data; - return `data:${mimeType}${data.base64 ? ';base64' : ''},${body}`; + return `${key}=${value}`; + }); + + const normalizedMediaType = [ + ...attributes, + base64 ? 'base64' : '' + ].filter(Boolean); + + if (normalizedMediaType.length !== 0 || mimeType) { + normalizedMediaType.unshift(mimeType); + } + + return `data:${normalizedMediaType.join(';')},${base64 ? body.trim() : body}`; }; const normalizeUrl = (urlString, options) => { diff --git a/test.js b/test.js index 859867f..b65347f 100644 --- a/test.js +++ b/test.js @@ -206,15 +206,23 @@ test('remove duplicate pathname slashes', t => { }); test('data URL', t => { - t.throws(() => { - normalizeUrl('data:text/plain;charset=UTF-8;,foo'); - }, 'Invalid URL: data:text/plain;charset=UTF-8;,foo'); + // Invalid URL. + t.throws(() => normalizeUrl('data:'), 'Invalid URL: data:'); + + // Normalize away trailing semicolon. + t.is(normalizeUrl('data:text/plain;charset=UTF-8;,foo'), 'data:text/plain;charset=utf-8,foo'); // Empty MIME type. t.is(normalizeUrl('data:,'), 'data:,'); + // Empty MIME type with charset. + t.is(normalizeUrl('data:;charset=utf-8,foo'), 'data:;charset=utf-8,foo'); + // Lowercase the MIME type. - t.is(normalizeUrl('data:TEXT/plain;charset=UTF-8,foo'), 'data:text/plain;charset=utf-8,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'); // 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'); From a795c236734546e5202c5257fd228005e3eb4f78 Mon Sep 17 00:00:00 2001 From: fisker Date: Wed, 18 Sep 2019 11:34:10 +0800 Subject: [PATCH 13/17] remove unnecessary filter --- index.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index e769356..d14d3fa 100644 --- a/index.js +++ b/index.js @@ -39,9 +39,12 @@ const normalizeDataURL = urlString => { }); const normalizedMediaType = [ - ...attributes, - base64 ? 'base64' : '' - ].filter(Boolean); + ...attributes + ]; + + if (base64) { + normalizedMediaType.push('base64'); + } if (normalizedMediaType.length !== 0 || mimeType) { normalizedMediaType.unshift(mimeType); From 36aafd7df079592f2e6992d18d4d9d19f1d3a277 Mon Sep 17 00:00:00 2001 From: fisker Date: Wed, 18 Sep 2019 11:35:10 +0800 Subject: [PATCH 14/17] Handle possible empty attribute --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index d14d3fa..20ab9de 100644 --- a/index.js +++ b/index.js @@ -28,7 +28,7 @@ const normalizeDataURL = urlString => { const attributes = mediaType .filter(Boolean) .map(attribute => { - let [key, value] = attribute.split('=').map(string => string.trim()); + let [key, value = ''] = attribute.split('=').map(string => string.trim()); // Lowercase `charset` if (key === 'charset') { From e7fc28fd2a73534c74254d2de6ddc2441fbd1937 Mon Sep 17 00:00:00 2001 From: fisker Date: Wed, 18 Sep 2019 14:08:39 +0800 Subject: [PATCH 15/17] Add `data URLs` info --- index.d.ts | 2 +- readme.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 6257bf1..3a4e851 100644 --- a/index.d.ts +++ b/index.d.ts @@ -192,7 +192,7 @@ declare const normalizeUrl: { /** [Normalize](https://en.wikipedia.org/wiki/URL_normalization) a URL. - @param url - URL to normalize. + @param url - URL(including data URLs) to normalize. @example ``` diff --git a/readme.md b/readme.md index 4d9e5eb..5ff7886 100644 --- a/readme.md +++ b/readme.md @@ -33,7 +33,7 @@ normalizeUrl('HTTP://xn--xample-hva.com:80/?b=bar&a=foo'); Type: `string` -URL to normalize. +URL(including data URLs) to normalize. #### options From 624831836ee0c4c40d8a732e38d96ccca034183d Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 20 Sep 2019 15:35:29 +0700 Subject: [PATCH 16/17] Update readme.md --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 5ff7886..a851fdd 100644 --- a/readme.md +++ b/readme.md @@ -33,7 +33,7 @@ normalizeUrl('HTTP://xn--xample-hva.com:80/?b=bar&a=foo'); Type: `string` -URL(including data URLs) to normalize. +URL to normalize, including [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). #### options From 53c2f0d859422ad6f3a20836a8640c7aa9c5f299 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Fri, 20 Sep 2019 15:36:15 +0700 Subject: [PATCH 17/17] Update index.d.ts --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 3a4e851..7e332f2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -192,7 +192,7 @@ declare const normalizeUrl: { /** [Normalize](https://en.wikipedia.org/wiki/URL_normalization) a URL. - @param url - URL(including data URLs) to normalize. + @param url - URL to normalize, including [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). @example ```