forked from lix-project/lix-website
271 lines
6.2 KiB
JavaScript
271 lines
6.2 KiB
JavaScript
/*!
|
|
* media-typer
|
|
* Copyright(c) 2014 Douglas Christopher Wilson
|
|
* MIT Licensed
|
|
*/
|
|
|
|
/**
|
|
* RegExp to match *( ";" parameter ) in RFC 2616 sec 3.7
|
|
*
|
|
* parameter = token "=" ( token | quoted-string )
|
|
* token = 1*<any CHAR except CTLs or separators>
|
|
* separators = "(" | ")" | "<" | ">" | "@"
|
|
* | "," | ";" | ":" | "\" | <">
|
|
* | "/" | "[" | "]" | "?" | "="
|
|
* | "{" | "}" | SP | HT
|
|
* quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
|
|
* qdtext = <any TEXT except <">>
|
|
* quoted-pair = "\" CHAR
|
|
* CHAR = <any US-ASCII character (octets 0 - 127)>
|
|
* TEXT = <any OCTET except CTLs, but including LWS>
|
|
* LWS = [CRLF] 1*( SP | HT )
|
|
* CRLF = CR LF
|
|
* CR = <US-ASCII CR, carriage return (13)>
|
|
* LF = <US-ASCII LF, linefeed (10)>
|
|
* SP = <US-ASCII SP, space (32)>
|
|
* SHT = <US-ASCII HT, horizontal-tab (9)>
|
|
* CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
|
* OCTET = <any 8-bit sequence of data>
|
|
*/
|
|
var paramRegExp = /; *([!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+) *= *("(?:[ !\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u0020-\u007e])*"|[!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+) */g;
|
|
var textRegExp = /^[\u0020-\u007e\u0080-\u00ff]+$/
|
|
var tokenRegExp = /^[!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+$/
|
|
|
|
/**
|
|
* RegExp to match quoted-pair in RFC 2616
|
|
*
|
|
* quoted-pair = "\" CHAR
|
|
* CHAR = <any US-ASCII character (octets 0 - 127)>
|
|
*/
|
|
var qescRegExp = /\\([\u0000-\u007f])/g;
|
|
|
|
/**
|
|
* RegExp to match chars that must be quoted-pair in RFC 2616
|
|
*/
|
|
var quoteRegExp = /([\\"])/g;
|
|
|
|
/**
|
|
* RegExp to match type in RFC 6838
|
|
*
|
|
* type-name = restricted-name
|
|
* subtype-name = restricted-name
|
|
* restricted-name = restricted-name-first *126restricted-name-chars
|
|
* restricted-name-first = ALPHA / DIGIT
|
|
* restricted-name-chars = ALPHA / DIGIT / "!" / "#" /
|
|
* "$" / "&" / "-" / "^" / "_"
|
|
* restricted-name-chars =/ "." ; Characters before first dot always
|
|
* ; specify a facet name
|
|
* restricted-name-chars =/ "+" ; Characters after last plus always
|
|
* ; specify a structured syntax suffix
|
|
* ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
|
* DIGIT = %x30-39 ; 0-9
|
|
*/
|
|
var subtypeNameRegExp = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.-]{0,126}$/
|
|
var typeNameRegExp = /^[A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126}$/
|
|
var typeRegExp = /^ *([A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126})\/([A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}) *$/;
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
exports.format = format
|
|
exports.parse = parse
|
|
|
|
/**
|
|
* Format object to media type.
|
|
*
|
|
* @param {object} obj
|
|
* @return {string}
|
|
* @api public
|
|
*/
|
|
|
|
function format(obj) {
|
|
if (!obj || typeof obj !== 'object') {
|
|
throw new TypeError('argument obj is required')
|
|
}
|
|
|
|
var parameters = obj.parameters
|
|
var subtype = obj.subtype
|
|
var suffix = obj.suffix
|
|
var type = obj.type
|
|
|
|
if (!type || !typeNameRegExp.test(type)) {
|
|
throw new TypeError('invalid type')
|
|
}
|
|
|
|
if (!subtype || !subtypeNameRegExp.test(subtype)) {
|
|
throw new TypeError('invalid subtype')
|
|
}
|
|
|
|
// format as type/subtype
|
|
var string = type + '/' + subtype
|
|
|
|
// append +suffix
|
|
if (suffix) {
|
|
if (!typeNameRegExp.test(suffix)) {
|
|
throw new TypeError('invalid suffix')
|
|
}
|
|
|
|
string += '+' + suffix
|
|
}
|
|
|
|
// append parameters
|
|
if (parameters && typeof parameters === 'object') {
|
|
var param
|
|
var params = Object.keys(parameters).sort()
|
|
|
|
for (var i = 0; i < params.length; i++) {
|
|
param = params[i]
|
|
|
|
if (!tokenRegExp.test(param)) {
|
|
throw new TypeError('invalid parameter name')
|
|
}
|
|
|
|
string += '; ' + param + '=' + qstring(parameters[param])
|
|
}
|
|
}
|
|
|
|
return string
|
|
}
|
|
|
|
/**
|
|
* Parse media type to object.
|
|
*
|
|
* @param {string|object} string
|
|
* @return {Object}
|
|
* @api public
|
|
*/
|
|
|
|
function parse(string) {
|
|
if (!string) {
|
|
throw new TypeError('argument string is required')
|
|
}
|
|
|
|
// support req/res-like objects as argument
|
|
if (typeof string === 'object') {
|
|
string = getcontenttype(string)
|
|
}
|
|
|
|
if (typeof string !== 'string') {
|
|
throw new TypeError('argument string is required to be a string')
|
|
}
|
|
|
|
var index = string.indexOf(';')
|
|
var type = index !== -1
|
|
? string.substr(0, index)
|
|
: string
|
|
|
|
var key
|
|
var match
|
|
var obj = splitType(type)
|
|
var params = {}
|
|
var value
|
|
|
|
paramRegExp.lastIndex = index
|
|
|
|
while (match = paramRegExp.exec(string)) {
|
|
if (match.index !== index) {
|
|
throw new TypeError('invalid parameter format')
|
|
}
|
|
|
|
index += match[0].length
|
|
key = match[1].toLowerCase()
|
|
value = match[2]
|
|
|
|
if (value[0] === '"') {
|
|
// remove quotes and escapes
|
|
value = value
|
|
.substr(1, value.length - 2)
|
|
.replace(qescRegExp, '$1')
|
|
}
|
|
|
|
params[key] = value
|
|
}
|
|
|
|
if (index !== -1 && index !== string.length) {
|
|
throw new TypeError('invalid parameter format')
|
|
}
|
|
|
|
obj.parameters = params
|
|
|
|
return obj
|
|
}
|
|
|
|
/**
|
|
* Get content-type from req/res objects.
|
|
*
|
|
* @param {object}
|
|
* @return {Object}
|
|
* @api private
|
|
*/
|
|
|
|
function getcontenttype(obj) {
|
|
if (typeof obj.getHeader === 'function') {
|
|
// res-like
|
|
return obj.getHeader('content-type')
|
|
}
|
|
|
|
if (typeof obj.headers === 'object') {
|
|
// req-like
|
|
return obj.headers && obj.headers['content-type']
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Quote a string if necessary.
|
|
*
|
|
* @param {string} val
|
|
* @return {string}
|
|
* @api private
|
|
*/
|
|
|
|
function qstring(val) {
|
|
var str = String(val)
|
|
|
|
// no need to quote tokens
|
|
if (tokenRegExp.test(str)) {
|
|
return str
|
|
}
|
|
|
|
if (str.length > 0 && !textRegExp.test(str)) {
|
|
throw new TypeError('invalid parameter value')
|
|
}
|
|
|
|
return '"' + str.replace(quoteRegExp, '\\$1') + '"'
|
|
}
|
|
|
|
/**
|
|
* Simply "type/subtype+siffx" into parts.
|
|
*
|
|
* @param {string} string
|
|
* @return {Object}
|
|
* @api private
|
|
*/
|
|
|
|
function splitType(string) {
|
|
var match = typeRegExp.exec(string.toLowerCase())
|
|
|
|
if (!match) {
|
|
throw new TypeError('invalid media type')
|
|
}
|
|
|
|
var type = match[1]
|
|
var subtype = match[2]
|
|
var suffix
|
|
|
|
// suffix after last +
|
|
var index = subtype.lastIndexOf('+')
|
|
if (index !== -1) {
|
|
suffix = subtype.substr(index + 1)
|
|
subtype = subtype.substr(0, index)
|
|
}
|
|
|
|
var obj = {
|
|
type: type,
|
|
subtype: subtype,
|
|
suffix: suffix
|
|
}
|
|
|
|
return obj
|
|
}
|