std/mime
import std/mime to use it. Source: src/std/mime.iyi at v0.18.0.
The table is fixed and lives here. Crystal's MIME and Python's mimetypes read /etc/mime.types and its relatives, so the same name answers differently on two machines; this one reads no file and answers the same everywhere. The values are IANA's registrations where one exists and what browsers and Go's mime send where none does: .js and .mjs are text/javascript (RFC 9239), .xml is application/xml (RFC 7303 prefers it to text/xml), .ico is IANA's image/vnd.microsoft.icon rather than the older image/x-icon, which browsers accept as well. Every entry is checked against Python's table by bench/std_mime_exercise.sh, which lists the few deliberate differences and why.
Left out: registering types at run time, which would make the answer depend on what ran before, and sniffing a type from the bytes of a file. Extensions are looked up by the last dot of the base name only, so a.tar.gz is application/gzip, as every server answers it.
MIMEstruct
pub struct MIME
# Whether a body of this type shrinks under gzip or deflate: text,
# JSON, JavaScript, XML and their `+json`/`+xml` kin (SVG among them),
# YAML, TOML, scripts, WebAssembly, uncompressed fonts and archives,
# bitmaps and icons. Not images, audio and video that are already
# compressed, nor zip, gzip and the other archives, nor WOFF and WOFF2,
# which are compressed fonts. WebAssembly is included because its code
# roughly halves under gzip, which is why servers compress it.
def self.compressible?(media_type : String) : Bool
# The extensions a media type is written with, dotted and sorted
# ("text/html" is [".htm", ".html"]). Parameters are ignored and case
# does not matter; empty when the type is unknown. The array is a fresh
# copy the caller may change.
def self.extensions(media_type : String) : Array(String)
# The same, falling back to *default*.
def self.from_extension(name : String, default : String = "application/octet-stream") : String
# The media type for a file name or extension: ".html", "html",
# "index.HTML" and "a/b.tar.gz" (by its last extension) all work, in
# any case. A path whose base name has no dot has no extension; a bare
# word is taken as the extension itself. Nil when unknown.
def self.from_extension?(name : String) : String | ::Nil
def initialize
# RFC 9110 §8.3.1: `type/subtype` followed by `; name=value` parameters,
# whitespace allowed around the semicolons and at either end, a value a
# token or a quoted string whose backslash escapes are undone. Answers
# the type lowercased and the parameters with lowercased names and
# values as written; a name given twice keeps its last value. Nil when
# any part is not a token, a parameter has no `=`, or a quote is left
# open.
def self.parse?(value : String) : ::Tuple(String, Hash(String, String)) | ::Nil
# The media type with `; charset=utf-8` added when it is text, JSON,
# JavaScript or XML (`+json` and `+xml` included) and names no charset
# yet. Anything else, and anything that does not parse, comes back as
# given.
def self.with_charset(media_type : String) : String
end
Media types for naming and serving files: the type a file name's extension stands for, the extensions a type is written with, whether a type is worth compressing on the wire, the type with its charset, and RFC 9110's
type/subtype; name=valueparsed.