UNPKG

@i-xi-dev/mimetype

Version:

A JavaScript MIME type parser and serializer, implements the MIME type defined in WHATWG MIME Sniffing Standard.

619 lines (618 loc) 25.1 kB
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _MediaType_instances, _MediaType_typeName, _MediaType_subtypeName, _MediaType_parameters, _MediaType_original, _MediaType_format; import { Http, HttpUtils, StringEx } from "../deps.js"; const { HTTP_QUOTED_STRING_TOKEN, HTTP_TOKEN, HTTP_WHITESPACE, } = HttpUtils.Pattern; /** * 文字列の先頭からメディアタイプのタイプ名を抽出し返却 * * @param input 文字列 * @returns パース結果 */ function _collectTypeName(input) { const u002FIndex = input.indexOf("/"); let typeName = ""; if (u002FIndex >= 0) { typeName = input.substring(0, u002FIndex); } return { collected: typeName, progression: typeName.length, }; } /** * 文字列の先頭からメディアタイプのサブタイプ名を抽出し返却 * * @param input 文字列 * @returns パース結果 */ function _collectSubtypeName(input) { let subtypeName; let progression; let followingParameters = false; if (input.includes(";")) { // 「;」あり(パラメーターあり) const u003BIndex = input.indexOf(";"); subtypeName = input.substring(0, u003BIndex); progression = u003BIndex; followingParameters = true; } else { // パラメーター無し subtypeName = input; progression = input.length; } subtypeName = StringEx.trimEnd(subtypeName, HTTP_WHITESPACE); return { collected: subtypeName, progression, following: followingParameters, }; } /** * 文字列の先頭からメディアタイプのパラメーター値終端位置を抽出し返却 * * @param input 文字列 * @returns パラメーター値終端位置 */ function _detectPrameterValueEnd(input) { let valueEndIndex = -1; let parseEnd = false; const u003BIndex = input.indexOf(";"); if (u003BIndex >= 0) { valueEndIndex = u003BIndex; } if (valueEndIndex < 0) { valueEndIndex = input.length; parseEnd = true; } return { valueEndIndex, parseEnd, }; } /** * The object representation of MIME type. * The `MediaType` instances are immutable. */ export class MediaType { /** * @param typeName The type of the MIME type. * @param subtypeName The subtype of the MIME type. * @param parameters The parameters of the MIME type. * @param original The original string that was passed to the `fromString` method. * @throws {TypeError} The `typeName` is empty or contains invalid characters. * @throws {TypeError} The `subtypeName` is empty or contains invalid characters. * @throws {TypeError} The `parameters` contains duplicate parameters. */ constructor(typeName, subtypeName, parameters = [], original = "") { _MediaType_instances.add(this); /** * タイプ名 */ _MediaType_typeName.set(this, void 0); /** * サブタイプ名 */ _MediaType_subtypeName.set(this, void 0); /** * パラメーターの辞書 * パラメーター名の重複は許可しない */ _MediaType_parameters.set(this, void 0); /** * パース前の文字列 */ _MediaType_original.set(this, void 0); if (StringEx.matches(typeName, HTTP_TOKEN) !== true) { throw new TypeError("typeName"); } if (StringEx.matches(subtypeName, HTTP_TOKEN) !== true) { throw new TypeError("subtypeName"); } const parameterMap = new Map(parameters.map((entry) => { return [ entry[0].toLowerCase(), entry[1], ]; })); if (parameters.length !== parameterMap.size) { throw new TypeError("parameters"); } __classPrivateFieldSet(this, _MediaType_typeName, typeName.toLowerCase(), "f"); __classPrivateFieldSet(this, _MediaType_subtypeName, subtypeName.toLowerCase(), "f"); __classPrivateFieldSet(this, _MediaType_parameters, parameterMap, "f"); __classPrivateFieldSet(this, _MediaType_original, original, "f"); Object.freeze(this); } /** * The [type](https://mimesniff.spec.whatwg.org/#type) of this MIME type. * * @example * ```javascript * const mediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * mediaType.type; * // → "application" * ``` */ get type() { return __classPrivateFieldGet(this, _MediaType_typeName, "f"); } /** * The [subtype](https://mimesniff.spec.whatwg.org/#subtype) of this MIME type. * * @example * ```javascript * const mediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * mediaType.subtype; * // → "soap+xml" * ``` */ get subtype() { return __classPrivateFieldGet(this, _MediaType_subtypeName, "f"); } /** * The +suffix (structured syntax suffix) of this MIME type. * * @see [https://www.iana.org/assignments/media-type-structured-suffix](https://www.iana.org/assignments/media-type-structured-suffix) * * @example * ```javascript * const mediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * mediaType.suffix; * // → "+xml" * ``` */ get suffix() { if (this.subtype.includes("+")) { const subtype = this.subtype; return subtype.substring(subtype.lastIndexOf("+")); } return ""; } /** * The [essence](https://mimesniff.spec.whatwg.org/#mime-type-essence) of this MIME type. * * @example * ```javascript * const mediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * mediaType.essence; * // → "application/soap+xml" * ``` */ get essence() { return __classPrivateFieldGet(this, _MediaType_typeName, "f") + "/" + __classPrivateFieldGet(this, _MediaType_subtypeName, "f"); } /** * When this instance was generated by the `fromString` method, The original string that was passed to the `fromString` method; * Otherwise, A serialized string representation. */ get originalString() { if (__classPrivateFieldGet(this, _MediaType_original, "f") === "") { return this.toString(); } return __classPrivateFieldGet(this, _MediaType_original, "f"); } // static create = Object.freeze({ // applicationType(subtypeName: string, parameters?: Array<MediaType.Parameter>): MediaType { // return new MediaType("application", subtypeName, parameters); // }, // audio,example,font,image,message,model,multipart,text,video // }) as ; /** * Parses a string representation of a MIME type. * * @param text The string to be parsed. * @returns A `MediaType` instance. * @throws {TypeError} The `text` is not contain the [type](https://mimesniff.spec.whatwg.org/#type) of a MIME type. * @throws {TypeError} The extracted [subtype](https://mimesniff.spec.whatwg.org/#subtype) is empty or contains invalid characters. * @throws {TypeError} The extracted parameters contains duplicate parameters. * @see [https://mimesniff.spec.whatwg.org/#parsing-a-mime-type](https://mimesniff.spec.whatwg.org/#parsing-a-mime-type) */ static fromString(text) { const trimmedText = StringEx.trim(text, HTTP_WHITESPACE); let work = trimmedText; let i = 0; // [mimesniff 4.4.]-1,2 削除済 // [mimesniff 4.4.]-3 const { collected: typeName, progression: typeNameLength } = _collectTypeName(work); if (typeNameLength <= 0) { throw new TypeError("typeName"); } // [mimesniff 4.4.]-4,5 はコンストラクターではじかれる // [mimesniff 4.4.]-6 work = work.substring(typeNameLength + 1); i = i + typeNameLength + 1; // [mimesniff 4.4.]-7,8 const { collected: subtypeName, progression: subtypeNameEnd, following } = _collectSubtypeName(work); work = (following === true) ? work.substring(subtypeNameEnd) : ""; i = i + subtypeNameEnd; // [mimesniff 4.4.]-9 はコンストラクターではじかれる // [mimesniff 4.4.]-10 はコンストラクターで行う if (work.length <= 0) { return new MediaType(typeName, subtypeName, [], trimmedText.substring(0, i)); } // [mimesniff 4.4.]-11 const parameterEntries = []; while (work.length > 0) { // [mimesniff 4.4.]-11.1 work = work.substring(1); i = i + 1; // [mimesniff 4.4.]-11.2 const startHttpSpaces2 = StringEx.collectStart(work, HTTP_WHITESPACE); work = work.substring(startHttpSpaces2.length); i = i + startHttpSpaces2.length; // [mimesniff 4.4.]-11.3 const u003BIndex = work.indexOf(";"); const u003DIndex = work.indexOf("="); let delimIndex = -1; if ((u003BIndex >= 0) && (u003DIndex >= 0)) { delimIndex = Math.min(u003BIndex, u003DIndex); } else if (u003BIndex >= 0) { delimIndex = u003BIndex; } else if (u003DIndex >= 0) { delimIndex = u003DIndex; } let parameterName; if (delimIndex >= 0) { parameterName = work.substring(0, delimIndex); } else { parameterName = work; } work = work.substring(parameterName.length); i = i + parameterName.length; // [mimesniff 4.4.]-11.4 はコンストラクターで行う // [mimesniff 4.4.]-11.5.1 if (work.startsWith(";")) { continue; } // [mimesniff 4.4.]-11.5.2 if (work.startsWith("=")) { work = work.substring(1); i = i + 1; } // [mimesniff 4.4.]-11.6 if (work.length <= 0) { break; } // [mimesniff 4.4.]-11.7 let parameterValue; if (work.startsWith('"')) { // [mimesniff 4.4.]-11.8.1 const { collected, progression } = HttpUtils.collectHttpQuotedString(work); work = work.substring(progression); i = i + progression; parameterValue = collected; // [mimesniff 4.4.]-11.8.2 const { valueEndIndex, parseEnd } = _detectPrameterValueEnd(work); work = (parseEnd === true) ? "" : work.substring(valueEndIndex); i = i + valueEndIndex; } else { // [mimesniff 4.4.]-11.9.1 const { valueEndIndex, parseEnd } = _detectPrameterValueEnd(work); parameterValue = work.substring(0, valueEndIndex); work = (parseEnd === true) ? "" : work.substring(valueEndIndex); i = i + valueEndIndex; // [mimesniff 4.4.]-11.9.2 parameterValue = StringEx.trimEnd(parameterValue, HTTP_WHITESPACE); // [mimesniff 4.4.]-11.9.3 if (parameterValue.length <= 0) { continue; } } // [mimesniff 4.4.]-11.10 if (StringEx.matches(parameterName, HTTP_TOKEN) !== true) { continue; } if ((StringEx.matches(parameterValue, HTTP_QUOTED_STRING_TOKEN) !== true) && (parameterValue.length > 0)) { continue; } if (parameterEntries.some((param) => param[0] === parameterName)) { continue; } parameterEntries.push([ parameterName, parameterValue, ]); } return new MediaType(typeName, subtypeName, parameterEntries, trimmedText.substring(0, i)); } /** * Returns a serialized string representation. * * @override * @returns A serialized string representation. * @example * ```javascript * const mediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * mediaType.toString(); * // → 'application/soap+xml;charset=utf-8;action="https://example.com/example"' * ``` */ toString() { return __classPrivateFieldGet(this, _MediaType_instances, "m", _MediaType_format).call(this); } /** * Returns a serialized string representation. * * @returns A serialized string representation. * @example * ```javascript * const mediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * mediaType.toJSON(); * // → 'application/soap+xml;charset=utf-8;action="https://example.com/example"' * ``` */ toJSON() { return this.toString(); } /** * Returns a new iterator object that contains the names for each parameter in this MIME type. * * @returns A new iterator object. * @example * ```javascript * const mediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * [ ...mediaType.parameterNames() ]; * // → [ "charset", "action" ] * ``` */ parameterNames() { return __classPrivateFieldGet(this, _MediaType_parameters, "f").keys(); } /** * Returns a new iterator object that contains the name-value pairs for each parameter in this MIME type. * * @returns A new iterator object. * @example * ```javascript * const mediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * [ ...mediaType.parameters() ]; * // → [ ["charset", "utf-8"], ["action", "https://example.com/example"] ] * ``` */ parameters() { return __classPrivateFieldGet(this, _MediaType_parameters, "f").entries(); } // XXX sort parameters /** * Determines whether the MIME type represented by this instance is equal to the MIME type represented by other instance. * * @param other The other instance of `MediaType`. * @param options The `MediaType.CompareOptions` dictionary. * @returns If two MIME types are equal, `true`; Otherwise, `false`. * @example * ```javascript * const mediaTypeA = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * const mediaTypeB = MediaType.fromString('application/soap+xml; charset=utf-16;action="https://example.com/example"'); * mediaTypeA.equals(mediaTypeB); * // → false * * const mediaTypeC = MediaType.fromString('APPLICATION/SOAP+XML;ACTION="https://example.com/example";CHARSET=utf-8'); * mediaTypeA.equals(mediaTypeC); * // → true * * const mediaTypeD = MediaType.fromString('application/soap+xml; charset=UTF-8;action="https://example.com/example"'); * mediaTypeA.equals(mediaTypeD); * // → false * mediaTypeA.equals(mediaTypeD, { caseInsensitiveParameters: ["charset"] }); * // → true * ``` */ equals(other, options) { if (other instanceof MediaType) { if (options && Array.isArray(options.caseInsensitiveParameters)) { const thisParams = [...this.parameters()].map(([paramName, paramValue]) => { return [ paramName, options.caseInsensitiveParameters.includes(paramName) ? paramValue.toLowerCase() : paramValue, ]; }); const thisClone = new MediaType(this.type, this.subtype, thisParams); const objParams = [...other.parameters()].map(([paramName, paramValue]) => { return [ paramName, options.caseInsensitiveParameters.includes(paramName) ? paramValue.toLowerCase() : paramValue, ]; }); const objClone = new MediaType(other.type, other.subtype, objParams); return (__classPrivateFieldGet(thisClone, _MediaType_instances, "m", _MediaType_format).call(thisClone, true) === __classPrivateFieldGet(objClone, _MediaType_instances, "m", _MediaType_format).call(objClone, true)); } return (__classPrivateFieldGet(this, _MediaType_instances, "m", _MediaType_format).call(this, true) === __classPrivateFieldGet(other, _MediaType_instances, "m", _MediaType_format).call(other, true)); } return false; } /** * Returns whether this MIME type has the specified parameter. * * @param parameterName The parameter name. * @returns If this MIME type has the specified parameter, `true`; Otherwise, `false`. * @example * ```javascript * const mediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * mediaType.hasParameter("charset"); * // → true * * mediaType.hasParameter("foo"); * // → false * ``` */ hasParameter(parameterName) { const normalizedName = parameterName.toLowerCase(); return __classPrivateFieldGet(this, _MediaType_parameters, "f").has(normalizedName); } /** * Returns a value of a specified parameter of this MIME type. * * @param parameterName The parameter name. * @returns A parameter value. If the parameter does not exist, `null`. * @example * ```javascript * const mediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * mediaType.getParameterValue("charset"); * // → "https://example.com/example" * * mediaType.getParameterValue("foo"); * // → null * ``` */ getParameterValue(parameterName) { const normalizedName = parameterName.toLowerCase(); if (__classPrivateFieldGet(this, _MediaType_parameters, "f").has(normalizedName) !== true) { return null; } return __classPrivateFieldGet(this, _MediaType_parameters, "f").get(normalizedName); } /** * Returns a copy of this instance with the specified parameters. * * @param parameters The set of parameter name-value pairs. * @returns A new instance. * @throws {TypeError} The `parameters` contains duplicate parameters. * @example * ```javascript * const sourceMediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * const paramsModifiedClone = sourceMediaType.withParameters([ ["charset": "UTF-16"] ]); * paramsModifiedClone.toString(); * // → 'application/soap+xml;charset=UTF-16' * * sourceMediaType.toString(); * // → 'application/soap+xml;charset=utf-8;action="https://example.com/example"' * ``` */ withParameters(parameters) { return new MediaType(__classPrivateFieldGet(this, _MediaType_typeName, "f"), __classPrivateFieldGet(this, _MediaType_subtypeName, "f"), parameters); } /** * Returns a copy of this instance with no parameters. * * @returns A new instance. * @example * ```javascript * const sourceMediaType = MediaType.fromString('application/soap+xml; charset=utf-8;action="https://example.com/example"'); * * const paramsRemovedClone = sourceMediaType.withoutParameters(); * paramsRemovedClone.toString(); * // → 'application/soap+xml' * * sourceMediaType.toString(); * // → 'application/soap+xml;charset=utf-8;action="https://example.com/example"' * ``` */ withoutParameters() { return new MediaType(__classPrivateFieldGet(this, _MediaType_typeName, "f"), __classPrivateFieldGet(this, _MediaType_subtypeName, "f")); } // (await Body.blob()).type と同じになるはず? /** * @experimental * @param headers The `Headers` object of `Request` or `Response`. * @returns A `MediaType` instance. * @see {@link https://fetch.spec.whatwg.org/#content-type-header `Content-Type` header (Fetch standard)} */ static fromHeaders(headers) { const CHARSET = "charset"; // 5. if (headers.has(Http.Header.CONTENT_TYPE) !== true) { throw new Error("Content-Type field not found"); } // 4, 5. const typesString = headers.get(Http.Header.CONTENT_TYPE); const typeStrings = HttpUtils.valuesOfHeaderFieldValue(typesString); if (typeStrings.length <= 0) { throw new Error("Content-Type value not found"); } // 1, 2, 3. let textEncoding = ""; let mediaTypeEssence = ""; let mediaType = null; // 6. for (const typeString of typeStrings) { try { // 6.1. const tempMediaType = MediaType.fromString(typeString); // 6.3. mediaType = tempMediaType; // 6.4. if (mediaTypeEssence !== mediaType.essence) { // 6.4.1. textEncoding = ""; // 6.4.2. if (mediaType.hasParameter(CHARSET)) { textEncoding = mediaType.getParameterValue(CHARSET); } // 6.4.3. mediaTypeEssence = mediaType.essence; } else { // 6.5. if ((mediaType.hasParameter(CHARSET) !== true) && (textEncoding !== "")) { // TODO mediaType.withParameters() } } } catch (exception) { console.log(exception); // TODO 消す // 6.2. "*/*"はMediaType.fromStringでエラーにしている continue; } } // 7, 8. if (mediaType !== null) { return mediaType; } else { throw new Error("extraction failure"); } } } _MediaType_typeName = new WeakMap(), _MediaType_subtypeName = new WeakMap(), _MediaType_parameters = new WeakMap(), _MediaType_original = new WeakMap(), _MediaType_instances = new WeakSet(), _MediaType_format = function _MediaType_format(sortParameters = false) { const parameterNames = [...__classPrivateFieldGet(this, _MediaType_parameters, "f").keys()]; if (sortParameters === true) { parameterNames.sort(); } let parameters = ""; for (const parameterName of parameterNames) { parameters = parameters + ";" + parameterName + "="; const parameterValue = __classPrivateFieldGet(this, _MediaType_parameters, "f").get(parameterName); if ((StringEx.matches(parameterValue, HTTP_TOKEN) === true) || (parameterValue.length === 0)) { parameters = parameters + parameterValue; } else { parameters = parameters + '"' + parameterValue.replaceAll("\\", "\\\\").replaceAll('"', '\\"') + '"'; } } return __classPrivateFieldGet(this, _MediaType_typeName, "f") + "/" + __classPrivateFieldGet(this, _MediaType_subtypeName, "f") + parameters; };