-
Notifications
You must be signed in to change notification settings - Fork 4
/
mimeparse.js
311 lines (275 loc) · 11.3 KB
/
mimeparse.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
// mimeparse.js
//
// This module provides basic functions for handling mime-types. It can
// handle matching mime-types against a list of media-ranges. See section
// 14.1 of the HTTP specification [RFC 2616] for a complete explanation.
//
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
//
// A port to JavaScript of Joe Gregorio's MIME-Type Parser:
//
// http://code.google.com/p/mimeparse/
//
// Ported by J. Chris Anderson <[email protected]>, targeting the Spidermonkey runtime.
//
// To run the tests, open mimeparse-js-test.html in a browser.
// Ported from version 0.1.2
// Comments are mostly excerpted from the original.
var Mimeparse = (function() {
// private helpers
function strip(string) {
return string.replace(/^\s+/, '').replace(/\s+$/, '')
};
function parseRanges(ranges) {
var parsedRanges = [], rangeParts = ranges.split(",");
for (var i=0; i < rangeParts.length; i++) {
parsedRanges.push(publicMethods.parseMediaRange(rangeParts[i]))
};
return parsedRanges;
};
var publicMethods = {
// Carves up a mime-type and returns an Array of the
// [type, subtype, params] where "params" is a Hash of all
// the parameters for the media range.
//
// For example, the media range "application/xhtml;q=0.5" would
// get parsed into:
//
// ["application", "xhtml", { "q" : "0.5" }]
parseMimeType : function(mimeType) {
var fullType, typeParts, params = {}, parts = mimeType.split(';');
for (var i=0; i < parts.length; i++) {
var p = parts[i].split('=');
if (p.length == 2) {
params[strip(p[0])] = strip(p[1]);
}
};
fullType = parts[0].replace(/^\s+/, '').replace(/\s+$/, '');
if (fullType == '*') fullType = '*/*';
typeParts = fullType.split('/');
return [typeParts[0], typeParts[1], params];
},
// Carves up a media range and returns an Array of the
// [type, subtype, params] where "params" is a Object with
// all the parameters for the media range.
//
// For example, the media range "application/*;q=0.5" would
// get parsed into:
//
// ["application", "*", { "q" : "0.5" }]
//
// In addition this function also guarantees that there
// is a value for "q" in the params dictionary, filling it
// in with a proper default if necessary.
parseMediaRange : function(range) {
var q, parsedType = this.parseMimeType(range);
if (!parsedType[2]['q']) {
parsedType[2]['q'] = '1';
} else {
q = parseFloat(parsedType[2]['q']);
if (isNaN(q)) {
parsedType[2]['q'] = '1';
} else if (q > 1 || q < 0) {
parsedType[2]['q'] = '1';
}
}
return parsedType;
},
// Find the best match for a given mime-type against
// a list of media_ranges that have already been
// parsed by parseMediaRange(). Returns an array of
// the fitness value and the value of the 'q' quality
// parameter of the best match, or (-1, 0) if no match
// was found. Just as for qualityParsed(), 'parsed_ranges'
// must be a list of parsed media ranges.
fitnessAndQualityParsed : function(mimeType, parsedRanges) {
var bestFitness = -1, bestFitQ = 0, target = this.parseMediaRange(mimeType);
var targetType = target[0], targetSubtype = target[1], targetParams = target[2];
for (var i=0; i < parsedRanges.length; i++) {
var parsed = parsedRanges[i];
var type = parsed[0], subtype = parsed[1], params = parsed[2];
if ((type == targetType || type == "*" || targetType == "*") &&
(subtype == targetSubtype || subtype == "*" || targetSubtype == "*")) {
var matchCount = 0;
for (param in targetParams) {
if (param != 'q' && params[param] && params[param] == targetParams[param]) {
matchCount += 1;
}
}
var fitness = (type == targetType) ? 100 : 0;
fitness += (subtype == targetSubtype) ? 10 : 0;
fitness += matchCount;
if (fitness > bestFitness) {
bestFitness = fitness;
bestFitQ = params["q"];
}
}
};
return [bestFitness, parseFloat(bestFitQ)];
},
// Find the best match for a given mime-type against
// a list of media_ranges that have already been
// parsed by parseMediaRange(). Returns the
// 'q' quality parameter of the best match, 0 if no
// match was found. This function bahaves the same as quality()
// except that 'parsedRanges' must be a list of
// parsed media ranges.
qualityParsed : function(mimeType, parsedRanges) {
return this.fitnessAndQualityParsed(mimeType, parsedRanges)[1];
},
// Returns the quality 'q' of a mime-type when compared
// against the media-ranges in ranges. For example:
//
// >>> Mimeparse.quality('text/html','text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
// 0.7
quality : function(mimeType, ranges) {
return this.qualityParsed(mimeType, parseRanges(ranges));
},
// Takes a list of supported mime-types and finds the best
// match for all the media-ranges listed in header. The value of
// header must be a string that conforms to the format of the
// HTTP Accept: header. The value of 'supported' is a list of
// mime-types.
//
// >>> bestMatch(['application/xbel+xml', 'text/xml'], 'text/*;q=0.5,*/*; q=0.1')
// 'text/xml'
bestMatch : function(supported, header) {
var parsedHeader = parseRanges(header);
var weighted = [];
for (var i=0; i < supported.length; i++) {
weighted.push([publicMethods.fitnessAndQualityParsed(supported[i], parsedHeader), i, supported[i]])
};
weighted.sort();
return weighted[weighted.length-1][0][1] ? weighted[weighted.length-1][2] : '';
}
}
return publicMethods;
})();
Mimeparse.tests = {
test_parseMediaRange : function() {
T(equals(["application", "xml", {"q" : "1"}],
Mimeparse.parseMediaRange("application/xml;q=1")));
T(equals(["application", "xml", {"q" : "1"}],
Mimeparse.parseMediaRange("application/xml")));
T(equals(["application", "xml", {"q" : "1"}],
Mimeparse.parseMediaRange("application/xml;q=")));
T(equals(["application", "xml", {"q" : "1"}],
Mimeparse.parseMediaRange("application/xml ; q=")));
T(equals(["application", "xml", {"q" : "1", "b" : "other"}],
Mimeparse.parseMediaRange("application/xml ; q=1;b=other")));
T(equals(["application", "xml", {"q" : "1", "b" : "other"}],
Mimeparse.parseMediaRange("application/xml ; q=2;b=other")));
// Java URLConnection class sends an Accept header that includes a single "*"
T(equals(["*", "*", {"q" : ".2"}],
Mimeparse.parseMediaRange(" *; q=.2")));
},
test_rfc_2616_example : function() {
var accept = "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5";
T(equals(1, Mimeparse.quality("text/html;level=1", accept)));
T(equals(0.7, Mimeparse.quality("text/html", accept)));
T(equals(0.3, Mimeparse.quality("text/plain", accept)));
T(equals(0.5, Mimeparse.quality("image/jpeg", accept)));
T(equals(0.4, Mimeparse.quality("text/html;level=2", accept)));
T(equals(0.7, Mimeparse.quality("text/html;level=3", accept)));
},
test_bestMatch : function() {
var mimeTypesSupported = ['application/xbel+xml', 'application/xml'];
// direct match
T(equals(Mimeparse.bestMatch(mimeTypesSupported, 'application/xbel+xml'), 'application/xbel+xml'));
// direct match with a q parameter
T(equals(Mimeparse.bestMatch(mimeTypesSupported, 'application/xbel+xml; q=1'), 'application/xbel+xml'));
// direct match of our second choice with a q parameter
T(equals(Mimeparse.bestMatch(mimeTypesSupported, 'application/xml; q=1'), 'application/xml'));
// match using a subtype wildcard
T(equals(Mimeparse.bestMatch(mimeTypesSupported, 'application/*; q=1'), 'application/xml'));
// match using a type wildcard
T(equals(Mimeparse.bestMatch(mimeTypesSupported, '*/*'), 'application/xml'));
mimeTypesSupported = ['application/xbel+xml', 'text/xml']
// match using a type versus a lower weighted subtype
T(equals(Mimeparse.bestMatch(mimeTypesSupported, 'text/*;q=0.5,*/*; q=0.1'), 'text/xml'));
// fail to match anything
T(equals(Mimeparse.bestMatch(mimeTypesSupported, 'text/html,application/atom+xml; q=0.9'), ''));
// common AJAX scenario
mimeTypesSupported = ['application/json', 'text/html']
T(equals(Mimeparse.bestMatch(mimeTypesSupported, 'application/json, text/javascript, */*'), 'application/json'));
// verify fitness ordering
T(equals(Mimeparse.bestMatch(mimeTypesSupported, 'application/json, text/html;q=0.9'), 'application/json'));
// test ordering preference (best supported is last)
T(equals(Mimeparse.bestMatch(['application/json', 'application/xml'], 'application/json, application/xml'), 'application/xml'));
T(equals(Mimeparse.bestMatch(['application/xml', 'application/json'], 'application/json, application/xml'), 'application/json'));
},
test_support_wildcards : function() {
var mime_types_supported = ['image/*', 'application/xml']
// match using a type wildcard
T(equals(Mimeparse.bestMatch(mime_types_supported, 'image/png'), 'image/*'));
// match using a wildcard for both requested and supported
T(equals(Mimeparse.bestMatch(mime_types_supported, 'image/*'), 'image/*'));
}
}
Mimeparse.runTests = function(outputFun) {
// from CouchDB's Test Runner (Apache 2.0 license)
function patchTest(fun) {
var source = fun.toString();
var output = "";
var i = 0;
var testMarker = "T("
while (i < source.length) {
var testStart = source.indexOf(testMarker, i);
if (testStart == -1) {
output = output + source.substring(i, source.length);
break;
}
var testEnd = source.indexOf(");", testStart);
var testCode = source.substring(testStart + testMarker.length, testEnd);
output += source.substring(i, testStart) + "T(" + testCode + "," + JSON.stringify(testCode);
i = testEnd;
}
try {
return eval("(" + output + ")");
} catch (e) {
return null;
}
}
function T(arg1, arg2) {
var message = (arg2 != null ? unescape(arg2) : arg1).toString();
if (arg1) {
outputFun('<strong style="color:#0d0;">OK:</strong> <tt>'+message+'</tt>');
} else {
// console.log(arg2)
outputFun('<strong style="color:#d00;">FAIL: <tt>'+message+'</tt></strong>');
}
}
function equals(a,b) {
if (a === b) return true;
try {
// console.log('equals')
// console.log(repr(a))
// console.log(repr(b))
return repr(a) === repr(b);
} catch (e) {
return false;
}
}
function repr(val) {
if (val === undefined) {
return null;
} else if (val === null) {
return "null";
} else {
return JSON.stringify(val);
}
}
outputFun("Starting tests.");
for (test in Mimeparse.tests) {
outputFun("Running <tt>"+test+"</tt>");
var testFun = Mimeparse.tests[test];
try {
testFun = patchTest(testFun) || testFun;
testFun();
} catch (e) {
outputFun('<strong style="color:#f00;">ERROR: '+e.toString()+'</strong>');
// console.log(e)
}
}
outputFun("Finished tests.");
};