Skip to content

Commit

Permalink
feat: add media_types (denoland#2286)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk committed May 31, 2022
1 parent 3e884da commit ae22ac4
Show file tree
Hide file tree
Showing 7 changed files with 9,570 additions and 0 deletions.
127 changes: 127 additions & 0 deletions media_types/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# std/media_types

Provides an API for handling media (MIME) types.

These APIs are inspired by the GoLang [`mime`](https://pkg.go.dev/mime) package
and [jshttp/mime-types](https://github.com/jshttp/mime-types), and is designed
to integrate and improve the APIs from
[deno.land/x/media_types](https://deno.land/x/media_types).

The `vendor` folder contains copy of the
[jshttp/mime-db](https://github.com/jshttp/mime-types) `db.json` file along with
its license.

## `contentType()`

Given a extension or media type, return a fully qualified header value for
setting a `Content-Type` or `Content-Disposition` header. The function will
process the value passed as a media type if it contains a `/`, otherwise will
attempt to match as an extension, with or without the leading `.`.

> Note: a side effect of `deno/x/media_types` was that you could pass a file
> name (e.g. `file.json`) and it would return the content type. This behavior is
> intentionally not supported here. If you want to get an extension for a file
> name, use `extname()` from `std/path/mod.ts` to determine the extension and
> pass it here.
```ts
import { contentType } from "https://deno.land/std@$STD_VERSION/media_types/mod.ts";

contentType(".json"); // `application/json; charset=UTF-8`
contentType("text/html"); // `text/html; charset=UTF-8`
contentType("txt"); // `text/plain; charset=UTF-8`
contentType("foo"); // undefined
contentType("file.json"); // undefined
```

## `extension()`

Given a media type, return the most relevant extension. If no extension can be
determined `undefined` is returned.

```ts
import { extension } from "https://deno.land/std@$STD_VERSION/media_types/mod.ts";

extension("text/plain"); // `txt`
extension("application/json"); // `json`
extension("text/html; charset=UTF-8"); // `html`
extension("application/foo"); // undefined
```

## `extensionsByType()`

Given a media type, return an array of extensions that can be applied. If no
extension can be determined `undefined` is returned.

```ts
import { extensionsByType } from "https://deno.land/std@$STD_VERSION/media_types/mod.ts";

extensionsByType("application/json"); // ["js", "mjs"]
extensionsByType("text/html; charset=UTF-8"); // ["html", "htm", "shtml"]
extensionsByType("application/foo"); // undefined
```

## `formatMediaType()`

Given a media type and optional parameters, return a spec compliant value. If
the parameters result in a non-compliant value, an empty string (`""`) is
returned.

```ts
import { formatMediaType } from "https://deno.land/std@$STD_VERSION/media_types/mod.ts";

formatMediaType("text/plain", { charset: "UTF-8" }); // `text/plain; charset=UTF-8`
```

## `getCharset()`

Given a media type, return the charset encoding for the value.

```ts
import { getCharset } from "https://deno.land/std@$STD_VERSION/media_types/mod.ts";

getCharset("text/plain"); // `UTF-8`
getCharset("application/foo"); // undefined
getCharset("application/news-checkgroups"); // `US-ASCII`
getCharset("application/news-checkgroups; charset=UTF-8"); // `UTF-8`
```

## `parseMediaType()`

Given a header value string, parse a value into a media type and any optional
parameters. If the supplied value is invalid, the function will throw.

```ts
import { parseMediaType } from "https://deno.land/std@$STD_VERSION/media_types/mod.ts";
import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts";

assertEquals(
parseMediaType("application/JSON"),
[
"application/json",
undefined,
],
);

assertEquals(
parseMediaType("text/html; charset=UTF-8"),
[
"application/json",
{ charset: "UTF-8" },
],
);
```

## `typeByExtension()`

Given an extension, return a media type. The extension can have a leading `.` or
not.

```ts
import { typeByExtension } from "https://deno.land/std@$STD_VERSION/media_types/mod.ts";

typeByExtension("js"); // `application/json`
typeByExtension(".HTML"); // `text/html`
typeByExtension("foo"); // undefined
typeByExtension("file.json"); // undefined
```
143 changes: 143 additions & 0 deletions media_types/_util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

/** Supporting functions for media_types that do not make part of the public
* API.
*
* @module
* @private
*/

export function consumeToken(v: string): [token: string, rest: string] {
const notPos = indexOf(v, isNotTokenChar);
if (notPos == -1) {
return [v, ""];
}
if (notPos == 0) {
return ["", v];
}
return [v.slice(0, notPos), v.slice(notPos)];
}

export function consumeValue(v: string): [value: string, rest: string] {
if (!v) {
return ["", v];
}
if (v[0] !== `"`) {
return consumeToken(v);
}
let value = "";
for (let i = 1; i < v.length; i++) {
const r = v[i];
if (r === `"`) {
return [value, v.slice(i + 1)];
}
if (r === "\\" && i + 1 < v.length && isTSpecial(v[i + 1])) {
value += v[i + 1];
i++;
continue;
}
if (r === "\r" || r === "\n") {
return ["", v];
}
value += v[i];
}
return ["", v];
}

export function consumeMediaParam(
v: string,
): [key: string, value: string, rest: string] {
let rest = v.trimStart();
if (!rest.startsWith(";")) {
return ["", "", v];
}
rest = rest.slice(1);
rest = rest.trimStart();
let param: string;
[param, rest] = consumeToken(rest);
param = param.toLowerCase();
if (!param) {
return ["", "", v];
}
rest = rest.slice(1);
rest = rest.trimStart();
const [value, rest2] = consumeValue(rest);
if (value == "" && rest2 === rest) {
return ["", "", v];
}
rest = rest2;
return [param, value, rest];
}

export function decode2331Encoding(v: string): string | undefined {
const sv = v.split(`'`, 3);
if (sv.length !== 3) {
return undefined;
}
const charset = sv[0].toLowerCase();
if (!charset) {
return undefined;
}
if (charset != "us-ascii" && charset != "utf-8") {
return undefined;
}
const encv = decodeURI(sv[2]);
if (!encv) {
return undefined;
}
return encv;
}

function indexOf<T>(s: Iterable<T>, fn: (s: T) => boolean): number {
let i = -1;
for (const v of s) {
i++;
if (fn(v)) {
return i;
}
}
return -1;
}

export function isIterator<T>(obj: unknown): obj is Iterable<T> {
if (obj == null) {
return false;
}
// deno-lint-ignore no-explicit-any
return typeof (obj as any)[Symbol.iterator] === "function";
}

export function isToken(s: string): boolean {
if (!s) {
return false;
}
return indexOf(s, isNotTokenChar) < 0;
}

function isNotTokenChar(r: string): boolean {
return !isTokenChar(r);
}

function isTokenChar(r: string): boolean {
const code = r.charCodeAt(0);
return code > 0x20 && code < 0x7f && !isTSpecial(r);
}

function isTSpecial(r: string): boolean {
return `()<>@,;:\\"/[]?=`.includes(r[0]);
}

const CHAR_CODE_SPACE = " ".charCodeAt(0);
const CHAR_CODE_TILDE = "~".charCodeAt(0);

export function needsEncoding(s: string): boolean {
for (const b of s) {
const charCode = b.charCodeAt(0);
if (
(charCode < CHAR_CODE_SPACE || charCode > CHAR_CODE_TILDE) && b !== "\t"
) {
return true;
}
}
return false;
}
67 changes: 67 additions & 0 deletions media_types/_util_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

import { assertEquals } from "../testing/asserts.ts";
import { consumeMediaParam, consumeToken, consumeValue } from "./_util.ts";

Deno.test({
name: "media_types::util - consumeToken()",
fn() {
const fixtures = [
["foo bar", "foo", " bar"],
["bar", "bar", ""],
["", "", ""],
[" foo", "", " foo"],
] as const;
for (const [fixture, token, rest] of fixtures) {
assertEquals(consumeToken(fixture), [token, rest]);
}
},
});

Deno.test({
name: "media_types::util - consumeValue()",
fn() {
const fixtures = [
["foo bar", "foo", " bar"],
["bar", "bar", ""],
[" bar ", "", " bar "],
[`"My value"end`, "My value", "end"],
[`"My value" end`, "My value", " end"],
[`"\\\\" rest`, "\\", " rest"],
[`"My \\" value"end`, 'My " value', "end"],
[`"\\" rest`, "", `"\\" rest`],
[`"C:\\dev\\go\\robots.txt"`, `C:\\dev\\go\\robots.txt`, ""],
[`"C:\\新建文件夹\\中文第二次测试.mp4"`, `C:\\新建文件夹\\中文第二次测试.mp4`, ""],
] as const;
for (const [fixture, value, rest] of fixtures) {
assertEquals(consumeValue(fixture), [value, rest]);
}
},
});

Deno.test({
name: "media_types::util - consumeMediaParam()",
fn() {
const fixtures = [
[" ; foo=bar", "foo", "bar", ""],
["; foo=bar", "foo", "bar", ""],
[";foo=bar", "foo", "bar", ""],
[";FOO=bar", "foo", "bar", ""],
[`;foo="bar"`, "foo", "bar", ""],
[`;foo="bar"; `, "foo", "bar", "; "],
[`;foo="bar"; foo=baz`, "foo", "bar", "; foo=baz"],
[` ; boundary=----CUT;`, "boundary", "----CUT", ";"],
[
` ; key=value; blah="value";name="foo" `,
"key",
"value",
`; blah="value";name="foo" `,
],
[`; blah="value";name="foo" `, "blah", "value", `;name="foo" `],
[`;name="foo" `, "name", "foo", ` `],
] as const;
for (const [fixture, key, value, rest] of fixtures) {
assertEquals(consumeMediaParam(fixture), [key, value, rest]);
}
},
});
Loading

0 comments on commit ae22ac4

Please sign in to comment.