| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| "use strict"; | ||
| const parse = require("./parser.js"); | ||
| const serialize = require("./serializer.js"); | ||
| const { | ||
| asciiLowercase, | ||
| solelyContainsHTTPTokenCodePoints, | ||
| soleyContainsHTTPQuotedStringTokenCodePoints | ||
| } = require("./utils.js"); | ||
|
|
||
| module.exports = class MIMEType { | ||
| constructor(string) { | ||
| string = String(string); | ||
| const result = parse(string); | ||
| if (result === null) { | ||
| throw new Error(`Could not parse MIME type string "${string}"`); | ||
| } | ||
|
|
||
| this._type = result.type; | ||
| this._subtype = result.subtype; | ||
| this._parameters = new MIMETypeParameters(result.parameters); | ||
| } | ||
|
|
||
| get essence() { | ||
| return `${this.type}/${this.subtype}`; | ||
| } | ||
|
|
||
| get type() { | ||
| return this._type; | ||
| } | ||
|
|
||
| set type(value) { | ||
| value = asciiLowercase(String(value)); | ||
|
|
||
| if (value.length === 0) { | ||
| throw new Error("Invalid type: must be a non-empty string"); | ||
| } | ||
| if (!solelyContainsHTTPTokenCodePoints(value)) { | ||
| throw new Error(`Invalid type ${value}: must contain only HTTP token code points`); | ||
| } | ||
|
|
||
| this._type = value; | ||
| } | ||
|
|
||
| get subtype() { | ||
| return this._subtype; | ||
| } | ||
|
|
||
| set subtype(value) { | ||
| value = asciiLowercase(String(value)); | ||
|
|
||
| if (value.length === 0) { | ||
| throw new Error("Invalid subtype: must be a non-empty string"); | ||
| } | ||
| if (!solelyContainsHTTPTokenCodePoints(value)) { | ||
| throw new Error(`Invalid subtype ${value}: must contain only HTTP token code points`); | ||
| } | ||
|
|
||
| this._subtype = value; | ||
| } | ||
|
|
||
| get parameters() { | ||
| return this._parameters; | ||
| } | ||
|
|
||
| toString() { | ||
| // The serialize function works on both "MIME type records" (i.e. the results of parse) and on this class, since | ||
| // this class's interface is identical. | ||
| return serialize(this); | ||
| } | ||
|
|
||
| isXML() { | ||
| return (this._subtype === "xml" && (this._type === "text" || this._type === "application")) || | ||
| this._subtype.endsWith("+xml"); | ||
| } | ||
| isHTML() { | ||
| return this._subtype === "html" && this._type === "text"; | ||
| } | ||
| }; | ||
|
|
||
| class MIMETypeParameters { | ||
| constructor(map) { | ||
| this._map = map; | ||
| } | ||
|
|
||
| get size() { | ||
| return this._map.size; | ||
| } | ||
|
|
||
| get(name) { | ||
| name = asciiLowercase(String(name)); | ||
| return this._map.get(name); | ||
| } | ||
|
|
||
| has(name) { | ||
| name = asciiLowercase(String(name)); | ||
| return this._map.has(name); | ||
| } | ||
|
|
||
| set(name, value) { | ||
| name = asciiLowercase(String(name)); | ||
| value = String(value); | ||
|
|
||
| if (!solelyContainsHTTPTokenCodePoints(name)) { | ||
| throw new Error(`Invalid MIME type parameter name "${name}": only HTTP token code points are valid.`); | ||
| } | ||
| if (!soleyContainsHTTPQuotedStringTokenCodePoints(value)) { | ||
| throw new Error(`Invalid MIME type parameter value "${value}": only HTTP quoted-string token code points are ` + | ||
| `valid.`); | ||
| } | ||
|
|
||
| return this._map.set(name, value); | ||
| } | ||
|
|
||
| clear() { | ||
| this._map.clear(); | ||
| } | ||
|
|
||
| delete(name) { | ||
| name = asciiLowercase(String(name)); | ||
| return this._map.delete(name); | ||
| } | ||
|
|
||
| forEach(callbackFn, thisArg) { | ||
| this._map.forEach(callbackFn, thisArg); | ||
| } | ||
|
|
||
| keys() { | ||
| return this._map.keys(); | ||
| } | ||
|
|
||
| values() { | ||
| return this._map.values(); | ||
| } | ||
|
|
||
| entries() { | ||
| return this._map.entries(); | ||
| } | ||
|
|
||
| [Symbol.iterator]() { | ||
| return this._map[Symbol.iterator](); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| "use strict"; | ||
| const { | ||
| removeLeadingAndTrailingASCIIWhitespace, | ||
| removeTrailingASCIIWhitespace, | ||
| isASCIIWhitespaceChar, | ||
| solelyContainsHTTPTokenCodePoints, | ||
| soleyContainsHTTPQuotedStringTokenCodePoints, | ||
| asciiLowercase | ||
| } = require("./utils.js"); | ||
|
|
||
| module.exports = input => { | ||
| input = removeLeadingAndTrailingASCIIWhitespace(input); | ||
|
|
||
| let position = 0; | ||
| let type = ""; | ||
| while (position < input.length && input[position] !== "/") { | ||
| type += input[position]; | ||
| ++position; | ||
| } | ||
|
|
||
| if (type.length === 0 || !solelyContainsHTTPTokenCodePoints(type)) { | ||
| return null; | ||
| } | ||
|
|
||
| if (position >= input.length) { | ||
| return null; | ||
| } | ||
|
|
||
| // Skips past "/" | ||
| ++position; | ||
|
|
||
| let subtype = ""; | ||
| while (position < input.length && input[position] !== ";") { | ||
| subtype += input[position]; | ||
| ++position; | ||
| } | ||
|
|
||
| subtype = removeTrailingASCIIWhitespace(subtype); | ||
|
|
||
| if (subtype.length === 0 || !solelyContainsHTTPTokenCodePoints(subtype)) { | ||
| return null; | ||
| } | ||
|
|
||
| const mimeType = { | ||
| type: asciiLowercase(type), | ||
| subtype: asciiLowercase(subtype), | ||
| parameters: new Map() | ||
| }; | ||
|
|
||
| while (position < input.length) { | ||
| // Skip past ";" | ||
| ++position; | ||
|
|
||
| while (isASCIIWhitespaceChar(input[position])) { | ||
| ++position; | ||
| } | ||
|
|
||
| let parameterName = ""; | ||
| while (position < input.length && input[position] !== ";" && input[position] !== "=") { | ||
| parameterName += input[position]; | ||
| ++position; | ||
| } | ||
| parameterName = asciiLowercase(parameterName); | ||
|
|
||
| if (position < input.length) { | ||
| if (input[position] === ";") { | ||
| continue; | ||
| } | ||
|
|
||
| // Skip past "=" | ||
| ++position; | ||
| } | ||
|
|
||
| let parameterValue = ""; | ||
| if (position < input.length) { | ||
| if (input[position] === "\"") { | ||
| ++position; | ||
|
|
||
| while (true) { | ||
| while (position < input.length && input[position] !== "\"" && input[position] !== "\\") { | ||
| parameterValue += input[position]; | ||
| ++position; | ||
| } | ||
|
|
||
| if (position < input.length && input[position] === "\\") { | ||
| ++position; | ||
| if (position < input.length) { | ||
| parameterValue += input[position]; | ||
| ++position; | ||
| continue; | ||
| } else { | ||
| parameterValue += "\\"; | ||
| break; | ||
| } | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| while (position < input.length && input[position] !== ";") { | ||
| ++position; | ||
| } | ||
| } else { | ||
| while (position < input.length && input[position] !== ";") { | ||
| parameterValue += input[position]; | ||
| ++position; | ||
| } | ||
|
|
||
| parameterValue = removeTrailingASCIIWhitespace(parameterValue); | ||
| } | ||
| } | ||
|
|
||
| if (parameterName.length > 0 && | ||
| parameterValue.length > 0 && | ||
| solelyContainsHTTPTokenCodePoints(parameterName) && | ||
| soleyContainsHTTPQuotedStringTokenCodePoints(parameterValue) && | ||
| !mimeType.parameters.has(parameterName)) { | ||
| mimeType.parameters.set(parameterName, parameterValue); | ||
| } | ||
| } | ||
|
|
||
| return mimeType; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| "use strict"; | ||
| const { solelyContainsHTTPTokenCodePoints } = require("./utils.js"); | ||
|
|
||
| module.exports = mimeType => { | ||
| let serialization = `${mimeType.type}/${mimeType.subtype}`; | ||
|
|
||
| if (mimeType.parameters.size === 0) { | ||
| return serialization; | ||
| } | ||
|
|
||
| for (let [name, value] of mimeType.parameters) { | ||
| serialization += ";"; | ||
| serialization += name; | ||
| serialization += "="; | ||
|
|
||
| if (!solelyContainsHTTPTokenCodePoints(value)) { | ||
| value = value.replace(/(["\\])/g, "\\$1"); | ||
| value = `"${value}"`; | ||
| } | ||
|
|
||
| serialization += value; | ||
| } | ||
|
|
||
| return serialization; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| "use strict"; | ||
|
|
||
| exports.removeLeadingAndTrailingASCIIWhitespace = string => { | ||
| return string.replace(/^[ \t\n\f\r]+/, "").replace(/[ \t\n\f\r]+$/, ""); | ||
| }; | ||
|
|
||
| exports.removeTrailingASCIIWhitespace = string => { | ||
| return string.replace(/[ \t\n\f\r]+$/, ""); | ||
| }; | ||
|
|
||
| exports.isASCIIWhitespaceChar = char => { | ||
| return char === " " || char === "\t" || char === "\n" || char === "\f" || char === "\r"; | ||
| }; | ||
|
|
||
| exports.solelyContainsHTTPTokenCodePoints = string => { | ||
| return /^[-!#$%&'*+.^_`|~A-Za-z0-9]+$/.test(string); | ||
| }; | ||
|
|
||
| exports.soleyContainsHTTPQuotedStringTokenCodePoints = string => { | ||
| return /^[\t\u0020-\u007E\u0080-\u00FF]+$/.test(string); | ||
| }; | ||
|
|
||
| exports.asciiLowercase = string => { | ||
| return string.replace(/[A-Z]/g, l => l.toLowerCase()); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,43 @@ | ||
| { | ||
| "name": "whatwg-mimetype", | ||
| "description": "Parses, serializes, and manipulates MIME types, according to the WHATWG MIME Sniffing Standard", | ||
| "keywords": [ | ||
| "content-type", | ||
| "mime type", | ||
| "mimesniff", | ||
| "http", | ||
| "whatwg" | ||
| ], | ||
| "version": "2.0.0-pre", | ||
| "author": "Domenic Denicola <d@domenic.me> (https://domenic.me/)", | ||
| "license": "MIT", | ||
| "repository": "jsdom/whatwg-mimetype", | ||
| "main": "lib/mime-type.js", | ||
| "files": [ | ||
| "lib/" | ||
| ], | ||
| "scripts": { | ||
| "test": "jest", | ||
| "coverage": "jest --coverage", | ||
| "lint": "eslint .", | ||
| "pretest": "node scripts/get-latest-platform-tests.js" | ||
| }, | ||
| "devDependencies": { | ||
| "eslint": "^4.12.1", | ||
| "jest": "^21.2.1", | ||
| "printable-string": "^0.3.0", | ||
| "request": "^2.83.0", | ||
| "whatwg-encoding": "^1.0.3" | ||
| }, | ||
| "jest": { | ||
| "coverageDirectory": "coverage", | ||
| "coverageReporters": [ | ||
| "lcov", | ||
| "text-summary" | ||
| ], | ||
| "testEnvironment": "node", | ||
| "testMatch": [ | ||
| "<rootDir>/test/**/*.js" | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "env": { | ||
| "node": true | ||
| }, | ||
| "rules": { | ||
| "no-process-env": "off", | ||
| "no-process-exit": "off", | ||
| "no-console": "off" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| "use strict"; | ||
|
|
||
| if (process.env.NO_UPDATE) { | ||
| process.exit(0); | ||
| } | ||
|
|
||
| const path = require("path"); | ||
| const fs = require("fs"); | ||
| const request = require("request"); | ||
|
|
||
| process.on("unhandledRejection", err => { | ||
| throw err; | ||
| }); | ||
|
|
||
| // Pin to specific version, reflecting the spec version in the readme. | ||
| // | ||
| // To get the latest commit: | ||
| // 1. Go to https://github.com/w3c/web-platform-tests/tree/master/mimesniff | ||
| // 2. Press "y" on your keyboard to get a permalink | ||
| // 3. Copy the commit hash | ||
| const commitHash = "e6a94100ccf5c481df3078cef5e4021769bad370"; | ||
|
|
||
| const urlPrefix = `https://raw.githubusercontent.com/w3c/web-platform-tests/${commitHash}` + | ||
| `/mimesniff/mime-types/resources/`; | ||
|
|
||
| const files = ["mime-types.json", "generated-mime-types.json"]; | ||
|
|
||
| for (const file of files) { | ||
| const url = urlPrefix + file; | ||
| const targetFile = path.resolve(__dirname, "..", "test", "web-platform-tests", file); | ||
| request(url).pipe(fs.createWriteStream(targetFile)); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| { | ||
| "env": { | ||
| "node": true, | ||
| "jasmine": true, | ||
| "jest": true | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| "use strict"; | ||
| const MIMEType = require(".."); | ||
|
|
||
| describe("Smoke tests via README intro example", () => { | ||
| let mimeType; | ||
| beforeEach(() => { | ||
| mimeType = new MIMEType(`Text/HTML;Charset="utf-8"`); | ||
| }); | ||
|
|
||
| it("serializes correctly", () => { | ||
| expect(mimeType.toString()).toEqual("text/html;charset=utf-8"); | ||
| }); | ||
|
|
||
| it("has the correct type, subtype, and essence", () => { | ||
| expect(mimeType.type).toEqual("text"); | ||
| expect(mimeType.subtype).toEqual("html"); | ||
| expect(mimeType.essence).toEqual("text/html"); | ||
| }); | ||
|
|
||
| it("has the correct parameters", () => { | ||
| expect(mimeType.parameters.size).toEqual(1); | ||
| expect(mimeType.parameters.has("charset")).toBe(true); | ||
| expect(mimeType.parameters.get("charset")).toEqual("utf-8"); | ||
| }); | ||
|
|
||
| it("responds to parameter setting", () => { | ||
| mimeType.parameters.set("charset", "windows-1252"); | ||
| expect(mimeType.parameters.get("charset")).toEqual("windows-1252"); | ||
| expect(mimeType.toString()).toEqual("text/html;charset=windows-1252"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Constructor behavior", () => { | ||
| it("converts incoming arguments into strings", () => { | ||
| const arg = { | ||
| toString() { | ||
| return "text/HTML"; | ||
| } | ||
| }; | ||
| const mimeType = new MIMEType(arg); | ||
|
|
||
| expect(mimeType.toString()).toEqual("text/html"); | ||
| }); | ||
|
|
||
| it("throws on unparseable MIME types", () => { | ||
| expect(() => new MIMEType("asdf")).toThrow(); | ||
| expect(() => new MIMEType("text/html™")).toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("type manipulation", () => { | ||
| let mimeType; | ||
| beforeEach(() => { | ||
| mimeType = new MIMEType("application/xml;foo=bar"); | ||
| }); | ||
|
|
||
| it("responds to type being set", () => { | ||
| mimeType.type = "text"; | ||
| expect(mimeType.type).toEqual("text"); | ||
| expect(mimeType.essence).toEqual("text/xml"); | ||
| expect(mimeType.toString()).toEqual("text/xml;foo=bar"); | ||
| }); | ||
|
|
||
| it("ASCII-lowercases incoming type strings", () => { | ||
| mimeType.type = "TeXT"; | ||
| expect(mimeType.type).toEqual("text"); | ||
| expect(mimeType.essence).toEqual("text/xml"); | ||
| expect(mimeType.toString()).toEqual("text/xml;foo=bar"); | ||
| }); | ||
|
|
||
| it("converts the value set to a string", () => { | ||
| mimeType.type = { | ||
| toString() { | ||
| return "TeXT"; | ||
| } | ||
| }; | ||
| expect(mimeType.type).toEqual("text"); | ||
| expect(mimeType.essence).toEqual("text/xml"); | ||
| expect(mimeType.toString()).toEqual("text/xml;foo=bar"); | ||
| }); | ||
|
|
||
| it("throws an error for non-HTTP token code points", () => { | ||
| // not exhaustive; maybe later | ||
| expect(() => { | ||
| mimeType.type = "/"; | ||
| }).toThrow(); | ||
| }); | ||
|
|
||
| it("throws an error for an empty string", () => { | ||
| expect(() => { | ||
| mimeType.type = ""; | ||
| }).toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("subtype manipulation", () => { | ||
| let mimeType; | ||
| beforeEach(() => { | ||
| mimeType = new MIMEType("application/xml;foo=bar"); | ||
| }); | ||
|
|
||
| it("responds to type being set", () => { | ||
| mimeType.subtype = "pdf"; | ||
| expect(mimeType.subtype).toEqual("pdf"); | ||
| expect(mimeType.essence).toEqual("application/pdf"); | ||
| expect(mimeType.toString()).toEqual("application/pdf;foo=bar"); | ||
| }); | ||
|
|
||
| it("ASCII-lowercases incoming type strings", () => { | ||
| mimeType.subtype = "PdF"; | ||
| expect(mimeType.subtype).toEqual("pdf"); | ||
| expect(mimeType.essence).toEqual("application/pdf"); | ||
| expect(mimeType.toString()).toEqual("application/pdf;foo=bar"); | ||
| }); | ||
|
|
||
| it("converts the value set to a string", () => { | ||
| mimeType.subtype = { | ||
| toString() { | ||
| return "PdF"; | ||
| } | ||
| }; | ||
| expect(mimeType.subtype).toEqual("pdf"); | ||
| expect(mimeType.essence).toEqual("application/pdf"); | ||
| expect(mimeType.toString()).toEqual("application/pdf;foo=bar"); | ||
| }); | ||
|
|
||
| it("throws an error for non-HTTP token code points", () => { | ||
| // not exhaustive; maybe later | ||
| expect(() => { | ||
| mimeType.subtype = "/"; | ||
| }).toThrow(); | ||
| }); | ||
|
|
||
| it("throws an error for an empty string", () => { | ||
| expect(() => { | ||
| mimeType.subtype = ""; | ||
| }).toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("Group-testing functions", () => { | ||
| test("isHTML", () => { | ||
| expect((new MIMEType("text/html")).isHTML()).toBe(true); | ||
| expect((new MIMEType("text/html;charset=utf-8")).isHTML()).toBe(true); | ||
| expect((new MIMEType("text/html;charset=utf-8;foo=bar")).isHTML()).toBe(true); | ||
|
|
||
| expect((new MIMEType("text/xhtml")).isHTML()).toBe(false); | ||
| expect((new MIMEType("application/html")).isHTML()).toBe(false); | ||
| expect((new MIMEType("application/xhtml+xml")).isHTML()).toBe(false); | ||
| }); | ||
|
|
||
| test("isXML", () => { | ||
| expect((new MIMEType("application/xml")).isXML()).toBe(true); | ||
| expect((new MIMEType("application/xml;charset=utf-8")).isXML()).toBe(true); | ||
| expect((new MIMEType("application/xml;charset=utf-8;foo=bar")).isXML()).toBe(true); | ||
|
|
||
| expect((new MIMEType("text/xml")).isXML()).toBe(true); | ||
| expect((new MIMEType("text/xml;charset=utf-8")).isXML()).toBe(true); | ||
| expect((new MIMEType("text/xml;charset=utf-8;foo=bar")).isXML()).toBe(true); | ||
|
|
||
| expect((new MIMEType("text/svg+xml")).isXML()).toBe(true); | ||
| expect((new MIMEType("text/svg+xml;charset=utf-8")).isXML()).toBe(true); | ||
| expect((new MIMEType("text/svg+xml;charset=utf-8;foo=bar")).isXML()).toBe(true); | ||
|
|
||
| expect((new MIMEType("application/xhtml+xml")).isXML()).toBe(true); | ||
| expect((new MIMEType("application/xhtml+xml;charset=utf-8")).isXML()).toBe(true); | ||
| expect((new MIMEType("application/xhtml+xml;charset=utf-8;foo=bar")).isXML()).toBe(true); | ||
|
|
||
| expect((new MIMEType("text/xhtml")).isXML()).toBe(false); | ||
| expect((new MIMEType("text/svg")).isXML()).toBe(false); | ||
| expect((new MIMEType("application/html")).isXML()).toBe(false); | ||
| expect((new MIMEType("application/xml+xhtml")).isXML()).toBe(false); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| "use strict"; | ||
| const encodingLabelToName = require("whatwg-encoding").labelToName; | ||
| const printableString = require("printable-string"); | ||
| const testCases = require("./web-platform-tests/mime-types.json"); | ||
| const generatedTestCases = require("./web-platform-tests/generated-mime-types.json"); | ||
| const parse = require("../lib/parser.js"); | ||
| const serialize = require("../lib/serializer.js"); | ||
|
|
||
| describe("mime-types.json", () => { | ||
| runTestCases(testCases); | ||
| }); | ||
|
|
||
| describe("generated-mime-types.json", () => { | ||
| runTestCases(generatedTestCases); | ||
| }); | ||
|
|
||
| function runTestCases(cases) { | ||
| for (const testCase of cases) { | ||
| if (typeof testCase === "string") { | ||
| // It's a comment | ||
| continue; | ||
| } | ||
|
|
||
| const printableVersion = printableString(testCase.input); | ||
| const testName = printableVersion !== testCase.input ? | ||
| `${testCase.input} (${printableString(testCase.input)})` : | ||
| testCase.input; | ||
|
|
||
| test(testName, () => { | ||
| const parsed = parse(testCase.input); | ||
|
|
||
| if (testCase.output === null) { | ||
| expect(parsed).toEqual(null); | ||
| } else { | ||
| const serialized = serialize(parsed); | ||
| expect(serialized).toEqual(testCase.output); | ||
|
|
||
| const charset = parsed.parameters.get("charset"); | ||
| const encoding = encodingLabelToName(charset); | ||
| if (testCase.encoding !== null && testCase.encoding !== undefined) { | ||
| expect(encoding).toEqual(testCase.encoding); | ||
| } else { | ||
| expect(encoding).toEqual(null); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } |