3 * Copyright(c) 2014 Douglas Christopher Wilson
8 * RegExp to match *( ";" parameter ) in RFC 2616 sec 3.7
10 * parameter = token "=" ( token | quoted-string )
11 * token = 1*<any CHAR except CTLs or separators>
12 * separators = "(" | ")" | "<" | ">" | "@"
13 * | "," | ";" | ":" | "\" | <">
14 * | "/" | "[" | "]" | "?" | "="
15 * | "{" | "}" | SP | HT
16 * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
17 * qdtext = <any TEXT except <">>
18 * quoted-pair = "\" CHAR
19 * CHAR = <any US-ASCII character (octets 0 - 127)>
20 * TEXT = <any OCTET except CTLs, but including LWS>
21 * LWS = [CRLF] 1*( SP | HT )
23 * CR = <US-ASCII CR, carriage return (13)>
24 * LF = <US-ASCII LF, linefeed (10)>
25 * SP = <US-ASCII SP, space (32)>
26 * SHT = <US-ASCII HT, horizontal-tab (9)>
27 * CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
28 * OCTET = <any 8-bit sequence of data>
30 var paramRegExp = /; *([!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+) *= *("(?:[ !\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u0020-\u007e])*"|[!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+) */g;
31 var textRegExp = /^[\u0020-\u007e\u0080-\u00ff]+$/
32 var tokenRegExp = /^[!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+$/
35 * RegExp to match quoted-pair in RFC 2616
37 * quoted-pair = "\" CHAR
38 * CHAR = <any US-ASCII character (octets 0 - 127)>
40 var qescRegExp = /\\([\u0000-\u007f])/g;
43 * RegExp to match chars that must be quoted-pair in RFC 2616
45 var quoteRegExp = /([\\"])/g;
48 * RegExp to match type in RFC 6838
50 * type-name = restricted-name
51 * subtype-name = restricted-name
52 * restricted-name = restricted-name-first *126restricted-name-chars
53 * restricted-name-first = ALPHA / DIGIT
54 * restricted-name-chars = ALPHA / DIGIT / "!" / "#" /
55 * "$" / "&" / "-" / "^" / "_"
56 * restricted-name-chars =/ "." ; Characters before first dot always
57 * ; specify a facet name
58 * restricted-name-chars =/ "+" ; Characters after last plus always
59 * ; specify a structured syntax suffix
60 * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
61 * DIGIT = %x30-39 ; 0-9
63 var subtypeNameRegExp = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.-]{0,126}$/
64 var typeNameRegExp = /^[A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126}$/
65 var typeRegExp = /^ *([A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126})\/([A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}) *$/;
71 exports.format = format
75 * Format object to media type.
82 function format(obj) {
83 if (!obj || typeof obj !== 'object') {
84 throw new TypeError('argument obj is required')
87 var parameters = obj.parameters
88 var subtype = obj.subtype
89 var suffix = obj.suffix
92 if (!type || !typeNameRegExp.test(type)) {
93 throw new TypeError('invalid type')
96 if (!subtype || !subtypeNameRegExp.test(subtype)) {
97 throw new TypeError('invalid subtype')
100 // format as type/subtype
101 var string = type + '/' + subtype
105 if (!typeNameRegExp.test(suffix)) {
106 throw new TypeError('invalid suffix')
109 string += '+' + suffix
113 if (parameters && typeof parameters === 'object') {
115 var params = Object.keys(parameters).sort()
117 for (var i = 0; i < params.length; i++) {
120 if (!tokenRegExp.test(param)) {
121 throw new TypeError('invalid parameter name')
124 string += '; ' + param + '=' + qstring(parameters[param])
132 * Parse media type to object.
134 * @param {string|object} string
139 function parse(string) {
141 throw new TypeError('argument string is required')
144 // support req/res-like objects as argument
145 if (typeof string === 'object') {
146 string = getcontenttype(string)
149 if (typeof string !== 'string') {
150 throw new TypeError('argument string is required to be a string')
153 var index = string.indexOf(';')
154 var type = index !== -1
155 ? string.substr(0, index)
160 var obj = splitType(type)
164 paramRegExp.lastIndex = index
166 while (match = paramRegExp.exec(string)) {
167 if (match.index !== index) {
168 throw new TypeError('invalid parameter format')
171 index += match[0].length
172 key = match[1].toLowerCase()
175 if (value[0] === '"') {
176 // remove quotes and escapes
178 .substr(1, value.length - 2)
179 .replace(qescRegExp, '$1')
185 if (index !== -1 && index !== string.length) {
186 throw new TypeError('invalid parameter format')
189 obj.parameters = params
195 * Get content-type from req/res objects.
202 function getcontenttype(obj) {
203 if (typeof obj.getHeader === 'function') {
205 return obj.getHeader('content-type')
208 if (typeof obj.headers === 'object') {
210 return obj.headers && obj.headers['content-type']
215 * Quote a string if necessary.
217 * @param {string} val
222 function qstring(val) {
223 var str = String(val)
225 // no need to quote tokens
226 if (tokenRegExp.test(str)) {
230 if (str.length > 0 && !textRegExp.test(str)) {
231 throw new TypeError('invalid parameter value')
234 return '"' + str.replace(quoteRegExp, '\\$1') + '"'
238 * Simply "type/subtype+siffx" into parts.
240 * @param {string} string
245 function splitType(string) {
246 var match = typeRegExp.exec(string.toLowerCase())
249 throw new TypeError('invalid media type')
253 var subtype = match[2]
256 // suffix after last +
257 var index = subtype.lastIndexOf('+')
259 suffix = subtype.substr(index + 1)
260 subtype = subtype.substr(0, index)