UNPKG

@remix-run/headers

Version:

A toolkit for working with HTTP headers in JavaScript

191 lines (190 loc) 6.12 kB
import {} from "./header-value.js"; import { parseParams } from "./param-values.js"; import { isIterable } from "./utils.js"; /** * The value of a `Accept` HTTP header. * * [MDN `Accept` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) * * [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2) */ export class Accept { #map; /** * @param init A string, iterable, or record to initialize the header */ constructor(init) { this.#map = new Map(); if (init) { if (typeof init === 'string') { for (let piece of init.split(/\s*,\s*/)) { let params = parseParams(piece); if (params.length < 1) continue; let mediaType = params[0][0]; let weight = 1; for (let i = 1; i < params.length; i++) { let [key, value] = params[i]; if (key === 'q') { weight = Number(value); break; } } this.#map.set(mediaType.toLowerCase(), weight); } } else if (isIterable(init)) { for (let mediaType of init) { if (Array.isArray(mediaType)) { this.#map.set(mediaType[0].toLowerCase(), mediaType[1]); } else { this.#map.set(mediaType.toLowerCase(), 1); } } } else { for (let mediaType of Object.getOwnPropertyNames(init)) { this.#map.set(mediaType.toLowerCase(), init[mediaType]); } } this.#sort(); } } #sort() { this.#map = new Map([...this.#map].sort((a, b) => b[1] - a[1])); } /** * An array of all media types in the header. */ get mediaTypes() { return Array.from(this.#map.keys()); } /** * An array of all weights (q values) in the header. */ get weights() { return Array.from(this.#map.values()); } /** * The number of media types in the `Accept` header. */ get size() { return this.#map.size; } /** * Returns `true` if the header matches the given media type (i.e. it is "acceptable"). * * @param mediaType The media type to check * @return `true` if the media type is acceptable, `false` otherwise */ accepts(mediaType) { return this.getWeight(mediaType) > 0; } /** * Gets the weight of a given media type. Also supports wildcards, so e.g. `text/*` will match `text/html`. * * @param mediaType The media type to get the weight of * @return The weight of the media type */ getWeight(mediaType) { let [type, subtype] = mediaType.toLowerCase().split('/'); for (let [key, value] of this) { let [t, s] = key.split('/'); if ((t === type || t === '*' || type === '*') && (s === subtype || s === '*' || subtype === '*')) { return value; } } return 0; } /** * Returns the most preferred media type from the given list of media types. * * @param mediaTypes The list of media types to choose from * @return The most preferred media type or `null` if none match */ getPreferred(mediaTypes) { let sorted = mediaTypes .map((mediaType) => [mediaType, this.getWeight(mediaType)]) .sort((a, b) => b[1] - a[1]); let first = sorted[0]; return first !== undefined && first[1] > 0 ? first[0] : null; } /** * Returns the weight of a media type. If it is not in the header verbatim, this returns `null`. * * @param mediaType The media type to get the weight of * @return The weight of the media type, or `null` if it is not in the header */ get(mediaType) { return this.#map.get(mediaType.toLowerCase()) ?? null; } /** * Sets a media type with the given weight. * * @param mediaType The media type to set * @param weight The weight of the media type (default: `1`) */ set(mediaType, weight = 1) { this.#map.set(mediaType.toLowerCase(), weight); this.#sort(); } /** * Removes the given media type from the header. * * @param mediaType The media type to remove */ delete(mediaType) { this.#map.delete(mediaType.toLowerCase()); } /** * Checks if a media type is in the header. * * @param mediaType The media type to check * @return `true` if the media type is in the header (verbatim), `false` otherwise */ has(mediaType) { return this.#map.has(mediaType.toLowerCase()); } /** * Removes all media types from the header. */ clear() { this.#map.clear(); } /** * Returns an iterator of all media type and weight pairs. * * @return An iterator of `[mediaType, weight]` tuples */ entries() { return this.#map.entries(); } [Symbol.iterator]() { return this.entries(); } /** * Invokes the callback for each media type and weight pair. * * @param callback The function to call for each pair * @param thisArg The value to use as `this` when calling the callback */ forEach(callback, thisArg) { for (let [mediaType, weight] of this) { callback.call(thisArg, mediaType, weight, this); } } /** * Returns the string representation of the header value. * * @return The header value as a string */ toString() { let pairs = []; for (let [mediaType, weight] of this.#map) { pairs.push(`${mediaType}${weight === 1 ? '' : `;q=${weight}`}`); } return pairs.join(','); } }