// Copyright 2018-2026 the Deno authors. MIT license. // This module is browser compatible. import { consumeMediaParam, decode2331Encoding } from "./_util.ts"; const SEMICOLON_REGEXP = /^\s*;\s*$/; /** * Parses the media type and any optional parameters, per * {@link https://www.rfc-editor.org/rfc/rfc1521.html | RFC 1521}. * * Media types are the values in `Content-Type` and `Content-Disposition` * headers. On success the function returns a tuple where the first element is * the media type and the second element is the optional parameters or * `undefined` if there are none. * * The function will throw if the parsed value is invalid. * * The returned media type will be normalized to be lower case, and returned * params keys will be normalized to lower case, but preserves the casing of * the value. * * @param type The media type to parse. * * @returns A tuple where the first element is the media type and the second * element is the optional parameters or `undefined` if there are none. * * @example Usage * ```ts * import { parseMediaType } from "../media-types/parse_media_type.ts"; * import { assertEquals } from "../assert/mod.ts"; * * assertEquals(parseMediaType("application/JSON"), ["application/json", undefined]); * assertEquals(parseMediaType("text/html; charset=UTF-8"), ["text/html", { charset: "UTF-8" }]); * ``` */ export function parseMediaType( type: string, ): [mediaType: string, params: Record | undefined] { const [base] = type.split(";") as [string]; const mediaType = base.toLowerCase().trim(); const params: Record = {}; // Map of base parameter name -> parameter name -> value // for parameters containing a '*' character. const continuation = new Map>(); type = type.slice(base.length); while (type.length) { type = type.trimStart(); if (type.length === 0) { break; } const [key, value, rest] = consumeMediaParam(type); if (!key) { if (SEMICOLON_REGEXP.test(rest)) { // ignore trailing semicolons break; } throw new TypeError( `Cannot parse media type: invalid parameter "${type}"`, ); } let pmap = params; const [baseName, rest2] = key.split("*"); if (baseName && rest2 !== undefined) { if (!continuation.has(baseName)) { continuation.set(baseName, {}); } pmap = continuation.get(baseName)!; } if (key in pmap) { throw new TypeError("Cannot parse media type: duplicate key"); } pmap[key] = value; type = rest; } // Stitch together any continuations or things with stars // (i.e. RFC 2231 things with stars: "foo*0" or "foo*") let str = ""; for (const [key, pieceMap] of continuation) { const singlePartKey = `${key}*`; const type = pieceMap[singlePartKey]; if (type) { const decv = decode2331Encoding(type); if (decv) { params[key] = decv; } continue; } str = ""; let valid = false; for (let n = 0;; n++) { const simplePart = `${key}*${n}`; let type = pieceMap[simplePart]; if (type) { valid = true; str += type; continue; } const encodedPart = `${simplePart}*`; type = pieceMap[encodedPart]; if (!type) { break; } valid = true; if (n === 0) { const decv = decode2331Encoding(type); if (decv) { str += decv; } } else { const decv = decodeURI(type); str += decv; } } if (valid) { params[key] = str; } } return [mediaType, Object.keys(params).length ? params : undefined]; }