Skip to content

Commit 26c539a

Browse files
authored
New parser, serializer, API, and package name
* Renames the package from "content-type-parser" to "whatwg-mimetype", as MIME type is the more general concept, and this is now implementing part of the WHATWG MIME Sniffing standard * Replaces the parser and serializer with the newly-specified one from whatwg/mimesniff@cc81ec4. This closes #3 as regular expressions are no longer used. * Overhauls the API to more or less match what is proposed in whatwg/mimesniff#43. Notably, the invariants of the MIME type model are now maintained more aggressively, and the parameters exist on a separate Map-like data structure. Also removes the isText() method, as it's much less interesting than the other two. * Switches from Mocha to Jest, and brings in the appropriate web platform test data files. All of this helps close #1, as it's now clear that this project has its own direction which is more standards-based and merging it with another project doesn't make much sense.
1 parent 00a5e0c commit 26c539a

20 files changed

+4834
-343
lines changed

Diff for: .eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/coverage/**

Diff for: .eslintrc.json

+62-18
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@
99
},
1010
"rules": {
1111
// Possible errors
12-
"comma-dangle": ["error", "never"],
12+
"for-direction": "error",
13+
"getter-return": "error",
14+
"no-await-in-loop": "error",
15+
"no-compare-neg-zero": "error",
1316
"no-cond-assign": ["error", "except-parens"],
1417
"no-console": "error",
15-
"no-constant-condition": "error",
18+
"no-constant-condition": ["error", { "checkLoops": false }],
1619
"no-control-regex": "off",
1720
"no-debugger": "error",
1821
"no-dupe-args": "error",
@@ -28,13 +31,15 @@
2831
"no-inner-declarations": "off",
2932
"no-invalid-regexp": "error",
3033
"no-irregular-whitespace": "error",
31-
"no-negated-in-lhs": "error",
3234
"no-obj-calls": "error",
35+
"no-prototype-builtins": "error",
3336
"no-regex-spaces": "error",
3437
"no-sparse-arrays": "error",
38+
"no-template-curly-in-string": "error",
3539
"no-unexpected-multiline": "error",
3640
"no-unreachable": "error",
3741
"no-unsafe-finally": "off",
42+
"no-unsafe-negation": "error",
3843
"use-isnan": "error",
3944
"valid-jsdoc": "off",
4045
"valid-typeof": "error",
@@ -43,6 +48,7 @@
4348
"accessor-pairs": "error",
4449
"array-callback-return": "error",
4550
"block-scoped-var": "off",
51+
"class-methods-use-this": "off",
4652
"complexity": "off",
4753
"consistent-return": "error",
4854
"curly": ["error", "all"],
@@ -56,7 +62,7 @@
5662
"no-case-declarations": "error",
5763
"no-div-regex": "off",
5864
"no-else-return": "error",
59-
"no-empty-function": "error",
65+
"no-empty-function": "off",
6066
"no-empty-pattern": "error",
6167
"no-eq-null": "error",
6268
"no-eval": "error",
@@ -65,18 +71,18 @@
6571
"no-extra-label": "error",
6672
"no-fallthrough": "error",
6773
"no-floating-decimal": "error",
74+
"no-global-assign": "error",
6875
"no-implicit-coercion": "error",
6976
"no-implicit-globals": "error",
70-
"no-implied-eval": "error",
71-
"no-invalid-this": "error",
77+
"no-implied-eval": "off",
78+
"no-invalid-this": "off", // meh
7279
"no-iterator": "error",
7380
"no-labels": ["error", { "allowLoop": true }],
7481
"no-lone-blocks": "error",
7582
"no-loop-func": "off",
7683
"no-magic-numbers": "off",
7784
"no-multi-spaces": "error",
7885
"no-multi-str": "error",
79-
"no-native-reassign": "error",
8086
"no-new": "error",
8187
"no-new-func": "error",
8288
"no-new-wrappers": "error",
@@ -86,7 +92,9 @@
8692
"no-process-env": "error",
8793
"no-proto": "error",
8894
"no-redeclare": "error",
95+
"no-restricted-properties": "off",
8996
"no-return-assign": ["error", "except-parens"],
97+
"no-return-await": "error",
9098
"no-script-url": "off",
9199
"no-self-assign": "error",
92100
"no-self-compare": "error",
@@ -98,10 +106,13 @@
98106
"no-useless-call": "error",
99107
"no-useless-concat": "error",
100108
"no-useless-escape": "error",
109+
"no-useless-return": "error",
101110
"no-void": "error",
102111
"no-warning-comments": "off",
103112
"no-with": "error",
113+
"prefer-promise-reject-errors": "error",
104114
"radix": ["error", "as-needed"],
115+
"require-await": "error",
105116
"vars-on-top": "off",
106117
"wrap-iife": ["error", "outside"],
107118
"yoda": ["error", "never"],
@@ -121,90 +132,117 @@
121132
"no-undef-init": "error",
122133
"no-undefined": "off",
123134
"no-unused-vars": "error",
124-
"no-use-before-define": ["error", "nofunc"],
135+
"no-use-before-define": "off",
125136

126137
// Node.js and CommonJS
127138
"callback-return": "off",
128139
"global-require": "error",
129140
"handle-callback-err": "error",
141+
"no-buffer-constructor": "error",
130142
"no-mixed-requires": ["error", true],
131143
"no-new-require": "error",
132144
"no-path-concat": "error",
133145
"no-process-exit": "error",
134-
"no-restricted-imports": "off",
135146
"no-restricted-modules": "off",
136147
"no-sync": "off",
137148

138149
// Stylistic Issues
150+
"array-bracket-newline": ["error", { "multiline": true }],
139151
"array-bracket-spacing": ["error", "never"],
152+
"array-element-newline": ["off"],
140153
"block-spacing": ["error", "always"],
141154
"brace-style": ["error", "1tbs", { "allowSingleLine": false }],
142155
"camelcase": ["error", { "properties": "always" }],
156+
"capitalized-comments": "off",
157+
"comma-dangle": ["error", "never"],
143158
"comma-spacing": ["error", { "before": false, "after": true }],
144159
"comma-style": ["error", "last"],
145160
"computed-property-spacing": ["error", "never"],
146161
"consistent-this": "off",
147162
"eol-last": "error",
163+
"func-call-spacing": ["error", "never"],
164+
"func-name-matching": ["error"],
148165
"func-names": "off",
149166
"func-style": ["error", "declaration"],
167+
"function-paren-newline": ["error", "multiline"],
150168
"id-blacklist": "off",
151169
"id-length": "off",
152170
"id-match": "off",
153-
"indent": ["error", 2, { "SwitchCase": 1 }],
171+
"indent": ["error", 2, { "SwitchCase": 1, "CallExpression": {"arguments": "first"}, "FunctionExpression": {"parameters": "first"}, "ignoredNodes": ["ConditionalExpression"] }],
154172
"jsx-quotes": "off",
155173
"key-spacing": ["error", { "beforeColon": false, "afterColon": true, "mode": "strict" }],
156174
"keyword-spacing": ["error", { "before": true, "after": true }],
175+
"line-comment-position": "off",
157176
"linebreak-style": ["error", "unix"],
158177
"lines-around-comment": "off",
159178
"max-depth": "off",
160179
"max-len": ["error", 120, { "ignoreUrls": true }],
180+
"max-lines": "off",
161181
"max-nested-callbacks": "off",
162182
"max-params": "off",
163183
"max-statements": "off",
164184
"max-statements-per-line": ["error", { "max": 1 }],
165-
"new-cap": ["error", { "capIsNewExceptions": ["USVString"] }],
185+
"multiline-ternary": ["error", "always-multiline"],
186+
"new-cap": ["error", { "capIsNewExceptions": ["USVString", "DOMString"] }],
166187
"new-parens": "error",
167-
"newline-after-var": "off",
168-
"newline-before-return": "off",
169188
"newline-per-chained-call": "off",
170189
"no-array-constructor": "error",
171190
"no-bitwise": "off",
172191
"no-continue": "off",
173192
"no-inline-comments": "off",
174193
"no-lonely-if": "error",
194+
"no-mixed-operators": [
195+
"error",
196+
{
197+
"groups": [
198+
["&", "|", "^", "~", "<<", ">>", ">>>"],
199+
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
200+
["&&", "||"],
201+
["in", "instanceof"]
202+
]
203+
}
204+
],
175205
"no-mixed-spaces-and-tabs": "error",
206+
"no-multi-assign": "off",
176207
"no-multiple-empty-lines": "error",
177208
"no-negated-condition": "off",
178209
"no-nested-ternary": "error",
179210
"no-new-object": "error",
180211
"no-plusplus": "off",
181212
"no-restricted-syntax": "off",
182-
"no-spaced-func": "error",
213+
"no-tabs": "error",
183214
"no-ternary": "off",
184215
"no-trailing-spaces": "error",
185216
"no-underscore-dangle": "off",
186217
"no-unneeded-ternary": "error",
187218
"no-whitespace-before-property": "error",
219+
"nonblock-statement-body-position": "error",
220+
"object-curly-newline": ["error", { "consistent": true }],
188221
"object-curly-spacing": ["error", "always"],
189222
"object-property-newline": "off",
190223
"one-var": ["error", "never"],
191224
"one-var-declaration-per-line": ["error", "initializations"],
192225
"operator-assignment": ["error", "always"],
193226
"operator-linebreak": ["error", "after"],
194227
"padded-blocks": ["error", "never"],
228+
"padding-line-between-statements": "off",
195229
"quote-props": ["error", "as-needed"],
196230
"quotes": ["error", "double", { "avoidEscape": true, "allowTemplateLiterals": true }],
197231
"require-jsdoc": "off",
198232
"semi": ["error", "always"],
199233
"semi-spacing": "error",
200-
"sort-imports": "off",
234+
"semi-style": "error",
235+
"sort-keys": "off",
201236
"sort-vars": "off",
202237
"space-before-blocks": ["error", "always"],
203238
"space-before-function-paren": ["error", { "anonymous": "always", "named": "never" }],
204239
"space-in-parens": ["error", "never"],
205240
"space-infix-ops": "error",
206241
"space-unary-ops": ["error", { "words": true, "nonwords": false }],
207242
"spaced-comment": ["error", "always", { "markers": ["///"] }],
243+
"switch-colon-spacing": "error",
244+
"template-tag-spacing": "error",
245+
"unicode-bom": "error",
208246
"wrap-regex": "off",
209247

210248
// ECMAScript 6
@@ -219,18 +257,24 @@
219257
"no-dupe-class-members": "error",
220258
"no-duplicate-imports": "error",
221259
"no-new-symbol": "error",
260+
"no-restricted-imports": "off",
222261
"no-this-before-super": "error",
223262
"no-useless-computed-key": "error",
224263
"no-useless-constructor": "error",
264+
"no-useless-rename": "error",
225265
"no-var": "error",
226266
"object-shorthand": "error",
227267
"prefer-arrow-callback": "error",
228-
"prefer-const": ["error", { "ignoreReadBeforeAssign": true }],
229-
"prefer-reflect": "off",
268+
"prefer-const": ["error", { "ignoreReadBeforeAssign": true, "destructuring": "all" }],
269+
"prefer-destructuring": ["error", { "VariableDeclarator": { "array": false, "object": true }, "AssignmentExpression": { "array": false, "object": false } }, { "enforceForRenamedProperties": false }],
270+
"prefer-numeric-literals": "error",
230271
"prefer-rest-params": "off",
231-
"prefer-spread": "off", // TODO with new Node versions
272+
"prefer-spread": "error",
232273
"prefer-template": "off",
233274
"require-yield": "error",
275+
"rest-spread-spacing": "error",
276+
"sort-imports": "off",
277+
"symbol-description": "error",
234278
"template-curly-spacing": ["error", "never"],
235279
"yield-star-spacing": ["error", "after"]
236280
}

Diff for: .gitattributes

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# lint requires lf line endings
2+
*.js text eol=lf

Diff for: .gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
/node_modules/
22
/npm-debug.log
3+
4+
/coverage/
5+
/test/web-platform-tests/*

Diff for: LICENSE.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright © 2016 Domenic Denicola <[email protected]>
1+
Copyright © 2017 Domenic Denicola <[email protected]>
22

33
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
44

Diff for: README.md

+72-34
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,98 @@
1-
# Parse `Content-Type` Header Strings
1+
# Parse, serialize, and manipulate MIME types
22

3-
This package will parse the [`Content-Type`](https://tools.ietf.org/html/rfc7231#section-3.1.1.1) header field into an introspectable data structure, whose parameters can be manipulated:
3+
This package will parse [MIME types](https://mimesniff.spec.whatwg.org/#understanding-mime-types) into a structured format, which can then be manipulated and serialized:
44

55
```js
6-
const contentTypeParser = require("content-type-parser");
6+
const MIMEType = require("content-type-parser");
77

8-
const contentType = contentTypeParser(`Text/HTML;Charset="utf-8"`);
8+
const mimeType = new MIMEType(`Text/HTML;Charset="utf-8"`);
99

10-
console.assert(contentType.toString() === "text/html;charset=utf-8");
10+
console.assert(mimeType.toString() === "text/html;charset=utf-8");
1111

12-
console.assert(contentType.type === "text");
13-
console.assert(contentType.subtype === "html");
14-
console.assert(contentType.get("charset") === "utf-8");
12+
console.assert(mimeType.type === "text");
13+
console.assert(mimeType.subtype === "html");
14+
console.assert(mimeType.essence === "text/html");
15+
console.assert(mimeType.parameters.get("charset") === "utf-8");
1516

16-
contentType.set("charset", "windows-1252");
17-
console.assert(contentType.get("charset") === "windows-1252");
18-
console.assert(contentType.toString() === "text/html;charset=windows-1252");
17+
mimeType.parameters.set("charset", "windows-1252");
18+
console.assert(mimeType.parameters.get("charset") === "windows-1252");
19+
console.assert(mimeType.toString() === "text/html;charset=windows-1252");
1920

20-
console.assert(contentType.isHTML() === true);
21-
console.assert(contentType.isXML() === false);
22-
console.assert(contentType.isText() === true);
21+
console.assert(mimeType.isHTML() === true);
22+
console.assert(mimeType.isXML() === false);
2323
```
2424

25-
Note how parsing will lowercase the type, subtype, and parameter name tokens (but not parameter values).
25+
Parsing is a fairly complex process; see [the specification](https://mimesniff.spec.whatwg.org/#parsing-a-mime-type) for details (and similarly [for serialization](https://mimesniff.spec.whatwg.org/#serializing-a-mime-type)).
2626

27-
If the passed string cannot be parsed as a content-type, `contentTypeParser` will return `null`.
27+
If the passed string cannot be parsed as a MIME type, the `MIMEType` constructor will throw.
2828

29-
## `ContentType` instance API
29+
This package's algorithms conform to those of the WHATWG [MIME Sniffing Standard](https://mimesniff.spec.whatwg.org/), and is aligned up to commit [cc81ec4](https://github.com/whatwg/mimesniff/commit/cc81ec48288944562c4554069da1d74a71e199fb).
3030

31-
This package's main module's default export will return an instance of the `ContentType` class, which has the following public APIs:
31+
## `MIMEType` API
32+
33+
This package's main module's default export is a class, `MIMEType`. Its constructor takes a string which it will attempt to parse into a MIME type; if parsing fails, an `Error` will be thrown.
3234

3335
### Properties
3436

35-
- `type`: the top-level media type, e.g. `"text"`
36-
- `subtype`: the subtype, e.g. `"html"`
37-
- `parameterList`: an array of `{ separator, key, value }` pairs representing the parameters. The `separator` field contains any whitespace, not just the `;` character.
37+
- `type`: the MIME type's [type](https://mimesniff.spec.whatwg.org/#mime-type-type), e.g. `"text"`
38+
- `subtype`: the MIME type's [subtype](https://mimesniff.spec.whatwg.org/#mime-type-subtype), e.g. `"html"`
39+
- `essence`: the MIME type's [essence](https://mimesniff.spec.whatwg.org/#mime-type-essence), e.g. `"text/html"`
40+
- `parameters`: an instance of `MIMETypeParameters`, containing this MIME type's [parameters](https://mimesniff.spec.whatwg.org/#mime-type-parameters)
41+
42+
`type` and `subtype` can be changed. They will be validated to be non-empty and only contain [HTTP token code points](https://mimesniff.spec.whatwg.org/#http-token-code-point).
43+
44+
`essence` is only a getter, and cannot be changed.
3845

39-
### Parameter manipulation
46+
`parameters` is also a getter, but the contents of the `MIMETypeParameters` object are mutable, as described below.
4047

41-
In general you should not directly manipulate `parameterList`. Instead, use the following APIs:
48+
### Methods
4249

43-
- `get("key")`: returns the value of the parameter with the given key, or `undefined` if no such parameter is present
44-
- `set("key", "value")`: adds the given key/value pair to the parameter list, or overwrites the existing value if an entry already existed
50+
- `toString()` serializes the MIME type to a string
51+
- `isHTML()`: returns true if this instance represents [a HTML MIME type](https://mimesniff.spec.whatwg.org/#html-mime-type)
52+
- `isXML()`: returns true if this instance represents [an XML MIME type](https://mimesniff.spec.whatwg.org/#xml-mime-type)
4553

46-
Both of these will lowercase the keys.
54+
_Note: the `isHTML()` and `isXML()` methods are speculative, and may be removed or changed in future major versions. See [whatwg/mimesniff#48](https://github.com/whatwg/mimesniff/issues/48) for brainstorming in this area. Currently we implement these mainly because they are useful in jsdom._
4755

48-
### MIME type tests
56+
## `MIMETypeParameters` API
4957

50-
- `isHTML()`: returns true if this instance's MIME type is [the HTML MIME type](https://html.spec.whatwg.org/multipage/infrastructure.html#html-mime-type), `"text/html"`
51-
- `isXML()`: returns true if this instance's MIME type is [an XML MIME type](https://html.spec.whatwg.org/multipage/infrastructure.html#xml-mime-type)
52-
- `isText()`: returns true if this instance's top-level media type is `"text"`
58+
The `MIMETypeParameters` class, instances of which are returned by `mimeType.parameters`, has equivalent surface API to a [JavaScript `Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map).
5359

54-
### Serialization
60+
However, `MIMETypeParameters` methods will always interpret their arguments as appropriate for MIME types, so e.g. parameter names will be lowercased, and attempting to set invalid characters will throw.
5561

56-
- `toString()` will return a canonicalized representation of the content-type, re-built from the parsed components
62+
Some examples:
63+
64+
```js
65+
const mimeType = new MIMEType(`x/x;a=b;c=D;E="F"`);
66+
67+
// Logs:
68+
// a b
69+
// c D
70+
// e F
71+
for (const [name, value] of mimeType.parameters) {
72+
console.log(name, value);
73+
}
74+
75+
console.assert(mimeType.parameters.has("a"));
76+
console.assert(mimeType.parameters.has("A"));
77+
console.assert(mimeType.parameters.get("A") === "b");
78+
79+
mimeType.parameters.set("Q", "X");
80+
console.assert(mimeType.parameters.get("q") === "X");
81+
console.assert(mimeType.toString() === "x/x;a=b;c=d;e=F;q=X");
82+
83+
// Throws:
84+
mimeType.parameters.set("@", "x");
85+
```
86+
87+
## Raw parsing/serialization APIs
88+
89+
If you want primitives on which to build your own API, you can get direct access to the parsing and serialization algorithms as follows:
90+
91+
```js
92+
const parse = require("content-type-parser/parser");
93+
const serialize = require("content-type-parser/serialize");
94+
```
5795

58-
## Credits
96+
`parse(string)` returns an object containing the `type` and `subtype` strings, plus `parameters`, which is a `Map`. This is roughly our equivalent of the spec's [MIME type record](https://mimesniff.spec.whatwg.org/#mime-type). If parsing fails, it instead returns `null`.
5997

60-
This package was originally based on the excellent work of [@nicolashenry](https://github.com/nicolashenry), [in jsdom](https://github.com/tmpvar/jsdom/blob/16fd85618f2705d181232f6552125872a37164bc/lib/jsdom/living/helpers/headers.js). It has since been pulled out into this separate package.
98+
`serialize(record)` operates on the such an object, giving back a string according to the serialization algorithm.

0 commit comments

Comments
 (0)