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 PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g
24 var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/
25 var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~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 QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g
36 * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6
38 var QUOTE_REGEXP = /([\\"])/g
41 * RegExp to match type in RFC 7231 sec 3.1.1.1
43 * media-type = type "/" subtype
47 var TYPE_REGEXP = /^[!#$%&'*+.^_`|~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 || !TYPE_REGEXP.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 (!TOKEN_REGEXP.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 // support req/res-like objects as argument
112 var header = typeof string === 'object'
113 ? getcontenttype(string)
116 if (typeof header !== 'string') {
117 throw new TypeError('argument string is required to be a string')
120 var index = header.indexOf(';')
121 var type = index !== -1
122 ? header.substr(0, index).trim()
125 if (!TYPE_REGEXP.test(type)) {
126 throw new TypeError('invalid media type')
129 var obj = new ContentType(type.toLowerCase())
137 PARAM_REGEXP.lastIndex = index
139 while ((match = PARAM_REGEXP.exec(header))) {
140 if (match.index !== index) {
141 throw new TypeError('invalid parameter format')
144 index += match[0].length
145 key = match[1].toLowerCase()
148 if (value[0] === '"') {
149 // remove quotes and escapes
151 .substr(1, value.length - 2)
152 .replace(QESC_REGEXP, '$1')
155 obj.parameters[key] = value
158 if (index !== header.length) {
159 throw new TypeError('invalid parameter format')
167 * Get content-type from req/res objects.
174 function getcontenttype (obj) {
177 if (typeof obj.getHeader === 'function') {
179 header = obj.getHeader('content-type')
180 } else if (typeof obj.headers === 'object') {
182 header = obj.headers && obj.headers['content-type']
185 if (typeof header !== 'string') {
186 throw new TypeError('content-type header is missing from object')
193 * Quote a string if necessary.
195 * @param {string} val
200 function qstring (val) {
201 var str = String(val)
203 // no need to quote tokens
204 if (TOKEN_REGEXP.test(str)) {
208 if (str.length > 0 && !TEXT_REGEXP.test(str)) {
209 throw new TypeError('invalid parameter value')
212 return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
216 * Class to represent a content type.
219 function ContentType (type) {
220 this.parameters = Object.create(null)