Repository: sindresorhus/normalize-url
Pull request #95 (2019-07-07 04:47:59)

Add data URL support
fixes: #94  & #84 

questions:

1. url `data:text/plain;charset=UTF-8;,foo` mentioned in #84, is not valid url accounding to `valid-data-url`, should I change Regex to loose strict one?
2. any more test cases need cover?
3. are we going to support other protocols like `file:` `ftp:` etc


<!-- Issuehunt content -->

---

<details>
<summary>
<b>IssueHunt Summary</b>
</summary>

### Referenced issues

This pull request has been submitted to:
- [#94: Normalize data URLs](https://issuehunt.io/repos/29094022/issues/94)
---

IssueHunt has been backed by the following sponsors. [Become a sponsor](https://issuehunt.io/membership/members)
</details>
<!-- /Issuehunt content-->

sindresorhus: >  url data:text/plain;charset=UTF-8;,foo mentioned in #84, is not valid url accounding to valid-data-url, should I change Regex to loose strict one?

Yes, but make sure it's a valid data URL according to the spec first: https://tools.ietf.org/html/rfc2397

> any more test cases need cover?

I can't really think of more cases.

> are we going to support other protocols like file: ftp: etc

Would be useful to support `file:` as it's quite common. Not interested in supporting `ftp` though.
sindresorhus: Would be good to mention data URL support in the readme and index.d.ts. I don't think it's obvious.
fisker: > Would be good to mention data URL support in the readme and index.d.ts. I don't think it's obvious.

Not sure how do you want to do this, add an example? or explain `url` parameter?
sindresorhus: > or explain url parameter?

This
fisker: @sindresorhus ready to review

Patch:
diff --git a/index.d.ts b/index.d.ts
index 6257bf1..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 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
 	```
diff --git a/index.js b/index.js
index ac386a0..20ab9de 100644
--- a/index.js
+++ b/index.js
@@ -6,6 +6,53 @@ 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:(.*?),(.*)$/);
+
+	if (!parts) {
+		throw new Error(`Invalid URL: ${urlString}`);
+	}
+
+	const mediaType = parts[1].split(';');
+	const body = parts[2];
+
+	let base64 = false;
+
+	if (mediaType[mediaType.length - 1] === 'base64') {
+		mediaType.pop();
+		base64 = true;
+	}
+
+	// Lowercase MIME type
+	const mimeType = (mediaType.shift() || '').toLowerCase();
+	const attributes = mediaType
+		.filter(Boolean)
+		.map(attribute => {
+			let [key, value = ''] = attribute.split('=').map(string => string.trim());
+
+			// Lowercase `charset`
+			if (key === 'charset') {
+				value = value.toLowerCase();
+			}
+
+			return `${key}=${value}`;
+		});
+
+	const normalizedMediaType = [
+		...attributes
+	];
+
+	if (base64) {
+		normalizedMediaType.push('base64');
+	}
+
+	if (normalizedMediaType.length !== 0 || mimeType) {
+		normalizedMediaType.unshift(mimeType);
+	}
+
+	return `data:${normalizedMediaType.join(';')},${base64 ? body.trim() : body}`;
+};
+
 const normalizeUrl = (urlString, options) => {
 	options = {
 		defaultProtocol: 'http:',
@@ -41,7 +88,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);
 	}
 
@@ -130,6 +177,12 @@ 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/readme.md b/readme.md
index 4d9e5eb..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 to normalize.
+URL to normalize, including [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs).
 
 #### options
 


Test patch:
diff --git a/test.js b/test.js
index 5fbc8b4..b65347f 100644
--- a/test.js
+++ b/test.js
@@ -204,3 +204,46 @@ 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('data URL', t => {
+	// 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,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');
+
+	// 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');
+
+	// 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', {
+		defaultProtocol: 'http:',
+		normalizeProtocol: true,
+		forceHttp: true,
+		stripHash: true,
+		stripWWW: true,
+		stripProtocol: true,
+		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');
+});