Source: lib/helper/content-type.js

/**
 * @module  helper-content-type
 * @desc    Helper module - Helper functions for the main module {@link module:whatwg-xhr whatwg-xhr}.
 * @version 1.0.0
 * @author  Essam A. El-Sherif
 */

/*
 * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1
 *
 * parameter     = token "=" ( token / quoted-string )
 * token         = 1*tchar
 * tchar         = "!" / "#" / "$" / "%" / "&" / "'" / "*"
 *               / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
 *               / DIGIT / ALPHA
 *               ; any VCHAR, except delimiters
 * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
 * qdtext        = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
 * obs-text      = %x80-FF
 * quoted-pair   = "\" ( HTAB / SP / VCHAR / obs-text )
 */
const PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g; // eslint-disable-line no-control-regex
const TEXT_REGEXP  = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/; // eslint-disable-line no-control-regex
const TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;

/*
 * RegExp to match quoted-pair in RFC 7230 sec 3.2.6
 *
 * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
 * obs-text    = %x80-FF
 */
const QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g // eslint-disable-line no-control-regex;

/*
 * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6
 */
const QUOTE_REGEXP = /([\\"])/g;

/*
 * RegExp to match type in RFC 7231 sec 3.1.1.1
 *
 * media-type = type "/" subtype
 * type       = token
 * subtype    = token
 */

const TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;

/**
 * @func   format
 * @static
 * @param  {object} obj
 * @return {string}
 * @desc   Format object to media type.
 */
export function format(obj){
	if(!obj || typeof obj !== 'object'){
		throw new TypeError('argument obj is required');
	}

	var parameters = obj.parameters;
	var type = obj.type;

	if(!type || !TYPE_REGEXP.test(type)){
		throw new TypeError('invalid type');
	}

	var string = type;

	// append parameters
	if (parameters && typeof parameters === 'object'){
		var param;
		var params = Object.keys(parameters);

		for (var i = 0; i < params.length; i++){
			param = params[i];

			if(!TOKEN_REGEXP.test(param)){
				throw new TypeError('invalid parameter name');
			}

			string += ';' + param + '=' + qstring(parameters[param]);
		}
	}

	return string
}

/**
 * @func   parse
 * @static
 * @param  {string|object} string
 * @return {object}
 * @desc   Parse media type to object.
 */
export function parse (string) {
	if(!string){
		throw new TypeError('argument string is required');
	}

	// support req/res-like objects as argument
	var header = typeof string === 'object'
		? getcontenttype(string)
		: string;

	if(typeof header !== 'string'){
		throw new TypeError('argument string is required to be a string');
	}

	var index = header.indexOf(';');
	var type = index !== -1
		? header.slice(0, index).trim()
		: header.trim();

	if (!TYPE_REGEXP.test(type)){
		throw new TypeError('invalid media type');
	}

	var obj = new ContentType(type.toLowerCase());

	// parse parameters
	if (index !== -1) {
		var key
		var match
		var value

		PARAM_REGEXP.lastIndex = index

		while ((match = PARAM_REGEXP.exec(header))) {
			if (match.index !== index) {
				throw new TypeError('invalid parameter format')
			}

			index += match[0].length
			key = match[1].toLowerCase()
			value = match[2]

			if (value.charCodeAt(0) === 0x22 /* " */) {
				// remove quotes
				value = value.slice(1, -1)

				// remove escapes
				if (value.indexOf('\\') !== -1) {
					value = value.replace(QESC_REGEXP, '$1')
				}
			}

			obj.parameters[key] = value
		}

		if (index !== header.length) {
			throw new TypeError('invalid parameter format')
		}
	}

	return obj
}

/*
 * Get content-type from req/res objects.
 *
 * @param {object}
 * @return {Object}
 * @private
 */

function getcontenttype (obj) {
	var header

	if (typeof obj.getHeader === 'function') {
		// res-like
		header = obj.getHeader('content-type')
	} else if (typeof obj.headers === 'object') {
		// req-like
		header = obj.headers && obj.headers['content-type']
	}

	if (typeof header !== 'string') {
		throw new TypeError('content-type header is missing from object')
	}

	return header
}

/*
 * Quote a string if necessary.
 *
 * @param {string} val
 * @return {string}
 * @private
 */

function qstring (val) {
	var str = String(val)

	// no need to quote tokens
	if (TOKEN_REGEXP.test(str)) {
		return str
	}

	if (str.length > 0 && !TEXT_REGEXP.test(str)) {
		throw new TypeError('invalid parameter value')
	}

	return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
}

/*
 * Class to represent a content type.
 * @private
 */
function ContentType (type) {
	this.parameters = Object.create(null)
	this.type = type
}

A Node.js implementation of the WHATWG XMLHttpRequest Living Standard for non-browser environments.