Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add data URL support #95

Merged
merged 17 commits into from Sep 20, 2019
54 changes: 53 additions & 1 deletion index.js
Expand Up @@ -6,6 +6,52 @@ 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
fisker marked this conversation as resolved.
Show resolved Hide resolved
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 = Boolean(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;
const {mimeType = ''} = data;

return `data:${mimeType}${data.base64 ? ';base64' : ''},${body}`;
};

const normalizeUrl = (urlString, options) => {
options = {
defaultProtocol: 'http:',
Expand Down Expand Up @@ -41,7 +87,7 @@ const normalizeUrl = (urlString, options) => {
const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString);

// Prepend protocol
if (!isRelativeUrl) {
if (!isRelativeUrl && !/^data?:/i.test(urlString)) {
fisker marked this conversation as resolved.
Show resolved Hide resolved
urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol);
}

Expand Down Expand Up @@ -130,6 +176,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(/\/$/, '');
}
Expand Down
35 changes: 35 additions & 0 deletions test.js
Expand Up @@ -204,3 +204,38 @@ 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 => {
t.throws(() => {
normalizeUrl('data:text/plain;charset=UTF-8;,foo');
}, 'Invalid URL: data:text/plain;charset=UTF-8;,foo');

// Empty MIME type.
t.is(normalizeUrl('data:,'), 'data:,');

// 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.
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');
});