@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
JavaScript
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;
};