@otterhttp/content-type
Advanced tools
Comparing version 0.3.0 to 0.4.0
@@ -12,8 +12,39 @@ import { IncomingHttpHeaders, OutgoingHttpHeaders } from 'node:http'; | ||
/** | ||
* Class to represent a content type. | ||
* Representation of a parsed MIME type. | ||
*/ | ||
declare class ContentType { | ||
parameters: Record<string, unknown>; | ||
type: string; | ||
constructor(type: string); | ||
/** | ||
* The top-level media type into which the data type falls, such as `video` or `text`. | ||
* e.g. in `application/json`, the type is `application`. | ||
*/ | ||
readonly type: string; | ||
/** | ||
* The whole subtype, such as `manifest+json` or `plain`. | ||
* e.g. in `text/conf+plain`, the subtype is `conf+plain`. | ||
*/ | ||
readonly subtype: string; | ||
/** | ||
* The subtype suffix, such as `json` or `plain`. | ||
* e.g. in `text/conf+plain`, the subtype suffix is `plain`. | ||
*/ | ||
readonly subtypeSuffix: string; | ||
/** | ||
* Optional parameters added to provide additional details. | ||
* For example, the `charset` parameter is often provided in HTTP contexts, e.g. | ||
* `Content-Type: application/json; charset=utf-8` | ||
*/ | ||
parameters: Record<string, string>; | ||
static parse(contentType: string): ContentType; | ||
/** | ||
* @internal | ||
*/ | ||
static fromValidatedInput(type: string, subtype: string, subtypeSuffix: string): ContentType; | ||
protected constructor(type: string, subtype: string, subtypeSuffix: string); | ||
toString(): string; | ||
hasWildcard(): boolean; | ||
isPlainText(): boolean; | ||
/** | ||
* The whole media type excluding parameters, such as `application/json` or `text/plain`. | ||
*/ | ||
get mediaType(): string; | ||
} | ||
@@ -25,3 +56,4 @@ /** | ||
type: string; | ||
parameters?: Record<string, unknown>; | ||
subtype: string; | ||
parameters?: Record<string, string>; | ||
}): string; | ||
@@ -32,4 +64,4 @@ /** | ||
declare function parse(value: TypeParseable): ContentType; | ||
declare function isPlainText({ type }: ContentType): boolean; | ||
declare function isPlainText({ type, subtypeSuffix }: ContentType): boolean; | ||
export { type TypeParseable, type TypeParseableObject, format, isPlainText, parse }; | ||
export { ContentType, type TypeParseable, type TypeParseableObject, format, isPlainText, parse }; |
// src/index.ts | ||
var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u0009\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u0009\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g; | ||
var TEXT_REGEXP = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/; | ||
var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/; | ||
var QESC_REGEXP = /\\([\u0009\u0020-\u00ff])/g; | ||
var QUOTE_REGEXP = /([\\"])/g; | ||
import { formatParameters, parseParameters, validateParameterNames } from "@otterhttp/parameters"; | ||
var WHITESPACE_CHAR_REGEXP = /[\u0009\u0020]/; | ||
var TOKEN_CHAR_REGEXP = /[!#$%&'*+.^_`|~0-9A-Za-z-]/; | ||
var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/; | ||
function qstring(val) { | ||
const str = String(val); | ||
if (TOKEN_REGEXP.test(str)) return str; | ||
if (str.length > 0 && !TEXT_REGEXP.test(str)) throw new TypeError("invalid parameter value"); | ||
return `"${str.replace(QUOTE_REGEXP, "\\$1")}"`; | ||
} | ||
function getContentType(obj) { | ||
@@ -26,19 +18,43 @@ let header; | ||
} | ||
var ContentType = class { | ||
constructor(type) { | ||
var ContentType = class _ContentType { | ||
static parse(contentType) { | ||
return parse(contentType); | ||
} | ||
/** | ||
* @internal | ||
*/ | ||
static fromValidatedInput(type, subtype, subtypeSuffix) { | ||
return new _ContentType(type, subtype, subtypeSuffix); | ||
} | ||
constructor(type, subtype, subtypeSuffix) { | ||
this.parameters = {}; | ||
this.type = type; | ||
this.subtype = subtype; | ||
this.subtypeSuffix = subtypeSuffix; | ||
} | ||
toString() { | ||
return `${this.type}/${this.subtype}${formatParameters(this.parameters)}`; | ||
} | ||
hasWildcard() { | ||
return this.type.indexOf("*") !== -1 || this.subtype.indexOf("*") !== -1; | ||
} | ||
isPlainText() { | ||
return isPlainText(this); | ||
} | ||
/** | ||
* The whole media type excluding parameters, such as `application/json` or `text/plain`. | ||
*/ | ||
get mediaType() { | ||
return `${this.type}/${this.subtype}`; | ||
} | ||
}; | ||
function format(obj) { | ||
if (!obj || typeof obj !== "object") throw new TypeError("argument obj is required"); | ||
const { parameters, type } = obj; | ||
if (!type || !TYPE_REGEXP.test(type)) throw new TypeError("invalid type"); | ||
let string = type; | ||
const { parameters, type, subtype } = obj; | ||
if (!type || !subtype) throw new TypeError("invalid type"); | ||
let string = `${type}/${subtype}`; | ||
if (!TYPE_REGEXP.test(string)) throw new TypeError("invalid type"); | ||
if (parameters && typeof parameters === "object") { | ||
const params = Object.keys(parameters).sort(); | ||
for (const param of params) { | ||
if (!TOKEN_REGEXP.test(param)) throw new TypeError("invalid parameter name"); | ||
string += `; ${param}=${qstring(parameters[param])}`; | ||
} | ||
validateParameterNames(Object.keys(parameters)); | ||
string += formatParameters(parameters); | ||
} | ||
@@ -48,27 +64,43 @@ return string; | ||
function parse(value) { | ||
if (!value) throw new TypeError("argument string is required"); | ||
const header = typeof value === "object" ? getContentType(value) : value; | ||
if (typeof header !== "string") throw new TypeError("argument string is required to be a string"); | ||
let index = header.indexOf(";"); | ||
const type = index !== -1 ? header.slice(0, index).trim() : header.trim(); | ||
if (!TYPE_REGEXP.test(type)) throw new TypeError("invalid media type"); | ||
const obj = new ContentType(type.toLowerCase()); | ||
if (index !== -1) { | ||
let key; | ||
let match; | ||
let value2; | ||
PARAM_REGEXP.lastIndex = index; | ||
while (match = PARAM_REGEXP.exec(header)) { | ||
if (match.index !== index) throw new TypeError("invalid parameter format"); | ||
index += match[0].length; | ||
key = match[1].toLowerCase(); | ||
value2 = match[2]; | ||
if (value2[0] === '"') { | ||
value2 = value2.slice(1, value2.length - 1).replace(QESC_REGEXP, "$1"); | ||
} | ||
obj.parameters[key] = value2; | ||
if (!value) throw new TypeError("argument `value` is required"); | ||
let header = typeof value === "object" ? getContentType(value) : value; | ||
if (typeof header !== "string") throw new TypeError("argument `value` must be string, request-like or response-like"); | ||
header = header.trim(); | ||
let currentIndex = 0; | ||
let slashIndex; | ||
for (; currentIndex < header.length; ++currentIndex) { | ||
const currentChar = header.charAt(currentIndex); | ||
if (currentChar === "/") { | ||
slashIndex = currentIndex; | ||
break; | ||
} | ||
if (index !== header.length) throw new TypeError("invalid parameter format"); | ||
if (!TOKEN_CHAR_REGEXP.test(currentChar)) throw new TypeError("invalid media type"); | ||
} | ||
return obj; | ||
if (typeof slashIndex === "undefined") throw new TypeError("invalid media type"); | ||
if (slashIndex === 0) throw new TypeError("invalid media type"); | ||
currentIndex += 1; | ||
let plusIndex; | ||
let endIndex; | ||
for (; currentIndex < header.length; ++currentIndex) { | ||
const currentChar = header.charAt(currentIndex); | ||
if (currentChar === ";" || WHITESPACE_CHAR_REGEXP.test(currentChar)) { | ||
if (currentIndex === slashIndex + 1) throw new TypeError("invalid media type"); | ||
endIndex = currentIndex; | ||
break; | ||
} | ||
if (currentChar === "+") { | ||
if (currentIndex === slashIndex + 1) throw new TypeError("invalid media type"); | ||
plusIndex = currentIndex; | ||
continue; | ||
} | ||
if (!TOKEN_CHAR_REGEXP.test(currentChar)) throw new TypeError("invalid media type"); | ||
} | ||
const lowercaseHeader = header.toLowerCase(); | ||
const type = lowercaseHeader.slice(0, slashIndex); | ||
const subtype = lowercaseHeader.slice(slashIndex + 1, endIndex); | ||
const subtypeSuffix = plusIndex == null ? subtype : lowercaseHeader.slice(plusIndex + 1, endIndex); | ||
const parsedRepresentation = ContentType.fromValidatedInput(type, subtype, subtypeSuffix); | ||
if (endIndex === void 0) return parsedRepresentation; | ||
parsedRepresentation.parameters = parseParameters(header.slice(endIndex)); | ||
return parsedRepresentation; | ||
} | ||
@@ -84,14 +116,9 @@ var applicationPlaintextWhitelist = /* @__PURE__ */ new Set([ | ||
]); | ||
function isPlainText({ type }) { | ||
if (type.startsWith("text/")) return true; | ||
if (!type.startsWith("application/")) return false; | ||
let index = 12; | ||
let start = index; | ||
for (; index < type.length; ++index) { | ||
if (type.charAt(index) === "+") start = index + 1; | ||
} | ||
const subtype = type.slice(start); | ||
return applicationPlaintextWhitelist.has(subtype); | ||
function isPlainText({ type, subtypeSuffix }) { | ||
if (type === "text") return true; | ||
if (type !== "application") return false; | ||
return applicationPlaintextWhitelist.has(subtypeSuffix); | ||
} | ||
export { | ||
ContentType, | ||
format, | ||
@@ -98,0 +125,0 @@ isPlainText, |
{ | ||
"name": "@otterhttp/content-type", | ||
"description": "content-type rewrite in TypeScript", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"license": "LGPL-3.0-or-later", | ||
@@ -25,3 +25,5 @@ "homepage": "https://otterhttp.lordfirespeed.dev", | ||
], | ||
"dependencies": {}, | ||
"dependencies": { | ||
"@otterhttp/parameters": "0.1.0" | ||
}, | ||
"scripts": { | ||
@@ -28,0 +30,0 @@ "build": "tsup" |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
11145
186
1
+ Added@otterhttp/parameters@0.1.0
+ Added@otterhttp/parameters@0.1.0(transitive)