3 * Copyright(c) 2015 Douglas Christopher Wilson
10 * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1
12 * parameter = token "=" ( token / quoted-string )
14 * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
15 * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
17 * ; any VCHAR, except delimiters
18 * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
19 * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
21 * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
23 var paramRegExp = /; *([!#$%&'\*\+\-\.\^_`\|~0-9A-Za-z]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'\*\+\-\.\^_`\|~0-9A-Za-z]+) */g
24 var textRegExp = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/
25 var tokenRegExp = /^[!#$%&'\*\+\-\.\^_`\|~0-9A-Za-z]+$/
28 * RegExp to match quoted-pair in RFC 7230 sec 3.2.6
30 * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
33 var qescRegExp = /\\([\u000b\u0020-\u00ff])/g
36 * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6
38 var quoteRegExp = /([\\"])/g
41 * RegExp to match type in RFC 6838
43 * media-type = type "/" subtype
47 var typeRegExp = /^[!#$%&'\*\+\-\.\^_`\|~0-9A-Za-z]+\/[!#$%&'\*\+\-\.\^_`\|~0-9A-Za-z]+$/
54 exports.format = format
58 * Format object to media type.
65 function format(obj) {
66 if (!obj || typeof obj !== 'object') {
67 throw new TypeError('argument obj is required')
70 var parameters = obj.parameters
73 if (!type || !typeRegExp.test(type)) {
74 throw new TypeError('invalid type')
80 if (parameters && typeof parameters === 'object') {
82 var params = Object.keys(parameters).sort()
84 for (var i = 0; i < params.length; i++) {
87 if (!tokenRegExp.test(param)) {
88 throw new TypeError('invalid parameter name')
91 string += '; ' + param + '=' + qstring(parameters[param])
99 * Parse media type to object.
101 * @param {string|object} string
106 function parse(string) {
108 throw new TypeError('argument string is required')
111 if (typeof string === 'object') {
112 // support req/res-like objects as argument
113 string = getcontenttype(string)
115 if (typeof string !== 'string') {
116 throw new TypeError('content-type header is missing from object');
120 if (typeof string !== 'string') {
121 throw new TypeError('argument string is required to be a string')
124 var index = string.indexOf(';')
125 var type = index !== -1
126 ? string.substr(0, index).trim()
129 if (!typeRegExp.test(type)) {
130 throw new TypeError('invalid media type')
135 var obj = new ContentType(type.toLowerCase())
138 paramRegExp.lastIndex = index
140 while (match = paramRegExp.exec(string)) {
141 if (match.index !== index) {
142 throw new TypeError('invalid parameter format')
145 index += match[0].length
146 key = match[1].toLowerCase()
149 if (value[0] === '"') {
150 // remove quotes and escapes
152 .substr(1, value.length - 2)
153 .replace(qescRegExp, '$1')
156 obj.parameters[key] = value
159 if (index !== -1 && index !== string.length) {
160 throw new TypeError('invalid parameter format')
167 * Get content-type from req/res objects.
174 function getcontenttype(obj) {
175 if (typeof obj.getHeader === 'function') {
177 return obj.getHeader('content-type')
180 if (typeof obj.headers === 'object') {
182 return obj.headers && obj.headers['content-type']
187 * Quote a string if necessary.
189 * @param {string} val
194 function qstring(val) {
195 var str = String(val)
197 // no need to quote tokens
198 if (tokenRegExp.test(str)) {
202 if (str.length > 0 && !textRegExp.test(str)) {
203 throw new TypeError('invalid parameter value')
206 return '"' + str.replace(quoteRegExp, '\\$1') + '"'
210 * Class to represent a content type.
213 function ContentType(type) {
214 this.parameters = Object.create(null)