Skip to content

Commit

Permalink
feat(media_types): single-export files (denoland#3001)
Browse files Browse the repository at this point in the history
  • Loading branch information
iuioiua committed Dec 19, 2022
1 parent 8cc6c18 commit fda52ca
Show file tree
Hide file tree
Showing 19 changed files with 737 additions and 694 deletions.
2 changes: 1 addition & 1 deletion http/file_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js

import { extname, posix } from "../path/mod.ts";
import { contentType } from "../media_types/mod.ts";
import { contentType } from "../media_types/content_type.ts";
import { serve, serveTls } from "./server.ts";
import { Status } from "./http_status.ts";
import { parse } from "../flags/mod.ts";
Expand Down
46 changes: 46 additions & 0 deletions media_types/_db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
import db from "./vendor/mime-db.v1.52.0.ts";
import { type DBEntry, extensions } from "./_util.ts";

export type KeyOfDb = keyof typeof db;

/** A map of the media type for a given extension */
export const types = new Map<string, KeyOfDb>();

/** Internal function to populate the maps based on the Mime DB. */
(function populateMaps() {
const preference = ["nginx", "apache", undefined, "iana"];

for (const type of Object.keys(db) as KeyOfDb[]) {
const mime = db[type] as DBEntry;
const exts = mime.extensions;

if (!exts || !exts.length) {
continue;
}

// @ts-ignore work around denoland/dnt#148
extensions.set(type, exts);

for (const ext of exts) {
const current = types.get(ext);
if (current) {
const from = preference.indexOf((db[current] as DBEntry).source);
const to = preference.indexOf(mime.source);

if (
current !== "application/octet-stream" &&
(from > to ||
// @ts-ignore work around denoland/dnt#148
(from === to && current.startsWith("application/")))
) {
continue;
}
}

types.set(ext, type);
}
}
})();

export { db };
9 changes: 9 additions & 0 deletions media_types/_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@
* @module
* @private
*/
export interface DBEntry {
source: string;
compressible?: boolean;
charset?: string;
extensions?: string[];
}

/** A map of extensions for a given media type. */
export const extensions = new Map<string, string[]>();

export function consumeToken(v: string): [token: string, rest: string] {
const notPos = indexOf(v, isNotTokenChar);
Expand Down
75 changes: 75 additions & 0 deletions media_types/content_type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
import { parseMediaType } from "./parse_media_type.ts";
import { typeByExtension } from "./type_by_extension.ts";
import { getCharset } from "./get_charset.ts";
import { formatMediaType } from "./format_media_type.ts";
import type { db } from "./_db.ts";

type DB = typeof db;
type ContentTypeToExtension = {
[K in keyof DB]: DB[K] extends { "extensions": readonly string[] }
? DB[K]["extensions"][number]
: never;
};

type KnownExtensionOrType =
| keyof ContentTypeToExtension
| ContentTypeToExtension[keyof ContentTypeToExtension]
| `.${ContentTypeToExtension[keyof ContentTypeToExtension]}`;

/**
* Given an extension or media type, return a full `Content-Type` or
* `Content-Disposition` header value.
*
* The function will treat the `extensionOrType` as a media type when it
* contains a `/`, otherwise it will process it as an extension, with or without
* the leading `.`.
*
* Returns `undefined` if unable to resolve the media type.
*
* > 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.
*
* @example
* ```ts
* import { contentType } from "https://deno.land/std@$STD_VERSION/media_types/content_type.ts";
*
* contentType(".json"); // `application/json; charset=UTF-8`
* contentType("text/html"); // `text/html; charset=UTF-8`
* contentType("text/html; charset=UTF-8"); // `text/html; charset=UTF-8`
* contentType("txt"); // `text/plain; charset=UTF-8`
* contentType("foo"); // undefined
* contentType("file.json"); // undefined
* ```
*/
export function contentType<
// Workaround to autocomplete for parameters: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-567871939
// deno-lint-ignore ban-types
T extends (string & {}) | KnownExtensionOrType,
>(
extensionOrType: T,
): Lowercase<T> extends KnownExtensionOrType ? string : string | undefined {
try {
const [mediaType, params = {}] = extensionOrType.includes("/")
? parseMediaType(extensionOrType)
: [typeByExtension(extensionOrType), undefined];
if (!mediaType) {
return undefined as Lowercase<T> extends KnownExtensionOrType ? string
: string | undefined;
}
if (!("charset" in params)) {
const charset = getCharset(mediaType);
if (charset) {
params.charset = charset;
}
}
return formatMediaType(mediaType, params);
} catch {
// just swallow returning undefined
}
return undefined as Lowercase<T> extends KnownExtensionOrType ? string
: string | undefined;
}
43 changes: 43 additions & 0 deletions media_types/content_type_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

import { contentType } from "./content_type.ts";
import { assertEquals } from "../testing/asserts.ts";

Deno.test({
name: "media_types - contentType()",
fn() {
const fixtures = [
[".json", "application/json; charset=UTF-8"],
["text/html", "text/html; charset=UTF-8"],
["txt", "text/plain; charset=UTF-8"],
["text/plain; charset=ISO-8859-1", "text/plain; charset=ISO-8859-1"],
["foo", undefined],
["file.json", undefined],
["application/foo", "application/foo"],
] as const;
for (const [fixture, expected] of fixtures) {
assertEquals(contentType(fixture), expected);
}
},
});

Deno.test({
name: "media_types - contentType()",
fn() {
let _str: string;
// For well-known content types, the return type is a string.
// string is assignable to string
_str = contentType(".json");
_str = contentType("text/html");
_str = contentType("txt");

// @ts-expect-error: string | undefined is not assignable to string
_str = contentType("text/plain; charset=ISO-8859-1");
// @ts-expect-error: string | undefined is not assignable to string
_str = contentType("foo");
// @ts-expect-error: string | undefined is not assignable to string
_str = contentType("file.json");
// @ts-expect-error: string | undefined is not assignable to string
_str = contentType("application/foo");
},
});
26 changes: 26 additions & 0 deletions media_types/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
import { extensionsByType } from "./extensions_by_type.ts";

/**
* For a given media type, return the most relevant extension, or `undefined`
* if no extension can be found.
*
* Extensions are returned without a leading `.`.
*
* @example
* ```ts
* import { extension } from "https://deno.land/std@$STD_VERSION/media_types/extension.ts";
*
* extension("text/plain"); // `txt`
* extension("application/json"); // `json`
* extension("text/html; charset=UTF-8"); // `html`
* extension("application/foo"); // undefined
* ```
*/
export function extension(type: string): string | undefined {
const exts = extensionsByType(type);
if (exts) {
return exts[0];
}
return undefined;
}
19 changes: 19 additions & 0 deletions media_types/extension_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

import { assertEquals } from "../testing/asserts.ts";
import { extension } from "./mod.ts";

Deno.test({
name: "media_types - extension()",
fn() {
const fixtures: [string, string | undefined][] = [
["image/gif", "gif"],
["application/javascript", "js"],
["text/html; charset=UTF-8", "html"],
["application/foo", undefined],
];
for (const [fixture, expected] of fixtures) {
assertEquals(extension(fixture), expected);
}
},
});
31 changes: 31 additions & 0 deletions media_types/extensions_by_type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
import { parseMediaType } from "./parse_media_type.ts";
import { extensions } from "./_util.ts";

export { extensions };

/**
* Returns the extensions known to be associated with the media type `type`.
* The returned extensions will each begin with a leading dot, as in `.html`.
*
* When `type` has no associated extensions, the function returns `undefined`.
*
* Extensions are returned without a leading `.`.
*
* @example
* ```ts
* import { extensionsByType } from "https://deno.land/std@$STD_VERSION/media_types/extensions_by_type.ts";
*
* extensionsByType("application/json"); // ["json", "map"]
* extensionsByType("text/html; charset=UTF-8"); // ["html", "htm", "shtml"]
* extensionsByType("application/foo"); // undefined
* ```
*/
export function extensionsByType(type: string): string[] | undefined {
try {
const [mediaType] = parseMediaType(type);
return extensions.get(mediaType);
} catch {
// just swallow errors, returning undefined
}
}
19 changes: 19 additions & 0 deletions media_types/extensions_by_type_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.

import { assertEquals } from "../testing/asserts.ts";
import { extensionsByType } from "./mod.ts";

Deno.test({
name: "media_types - extensionsByType()",
fn() {
const fixtures: [string, string[] | undefined][] = [
["image/gif", ["gif"]],
["application/javascript", ["js", "mjs"]],
["text/html; charset=UTF-8", ["html", "htm", "shtml"]],
["application/foo", undefined],
];
for (const [fixture, expected] of fixtures) {
assertEquals(extensionsByType(fixture), expected);
}
},
});
68 changes: 68 additions & 0 deletions media_types/format_media_type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
import { isIterator, isToken, needsEncoding } from "./_util.ts";

/** Serializes the media type and the optional parameters as a media type
* conforming to RFC 2045 and RFC 2616.
*
* The type and parameter names are written in lower-case.
*
* When any of the arguments results in a standard violation then the return
* value will be an empty string (`""`).
*
* @example
* ```ts
* import { formatMediaType } from "https://deno.land/std@$STD_VERSION/media_types/format_media_type.ts";
*
* formatMediaType("text/plain", { charset: "UTF-8" }); // `text/plain; charset=UTF-8`
* ```
*/
export function formatMediaType(
type: string,
param?: Record<string, string> | Iterable<[string, string]>,
): string {
let b = "";
const [major, sub] = type.split("/");
if (!sub) {
if (!isToken(type)) {
return "";
}
b += type.toLowerCase();
} else {
if (!isToken(major) || !isToken(sub)) {
return "";
}
b += `${major.toLowerCase()}/${sub.toLowerCase()}`;
}

if (param) {
param = isIterator(param) ? Object.fromEntries(param) : param;
const attrs = Object.keys(param);
attrs.sort();

for (const attribute of attrs) {
if (!isToken(attribute)) {
return "";
}
const value = param[attribute];
b += `; ${attribute.toLowerCase()}`;

const needEnc = needsEncoding(value);
if (needEnc) {
b += "*";
}
b += "=";

if (needEnc) {
b += `utf-8''${encodeURIComponent(value)}`;
continue;
}

if (isToken(value)) {
b += value;
continue;
}
b += `"${value.replace(/["\\]/gi, (m) => `\\${m}`)}"`;
}
}
return b;
}
Loading

0 comments on commit fda52ca

Please sign in to comment.