From d9b40dca83421e8cbdf527ccec69363285cae46e Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sun, 19 Mar 2023 11:09:21 -0700 Subject: [PATCH] Better header editor and added completion data --- src-tauri/icons/icon.icns | Bin 616124 -> 616124 bytes .../{editors => }/GraphQLEditor.tsx | 8 +- src-web/components/HeaderEditor.tsx | 52 +++++ src-web/components/ParameterEditor.tsx | 11 + src-web/components/RequestPane.tsx | 14 +- src-web/components/core/Editor/Editor.tsx | 12 +- src-web/components/core/Editor/extensions.ts | 11 +- .../core/Editor/genericCompletion.ts | 18 +- .../components/core/Editor/twig/extension.ts | 8 +- src-web/components/core/Input.tsx | 2 +- src-web/components/core/PairEditor.tsx | 158 ++++++++----- src-web/lib/data/charsets.ts | 121 ++++++++++ src-web/lib/data/connections.ts | 1 + src-web/lib/data/encodings.ts | 1 + src-web/lib/data/headerNames.ts | 35 +++ src-web/lib/data/mimetypes.ts | 208 ++++++++++++++++++ 16 files changed, 568 insertions(+), 92 deletions(-) rename src-web/components/{editors => }/GraphQLEditor.tsx (91%) create mode 100644 src-web/components/HeaderEditor.tsx create mode 100644 src-web/components/ParameterEditor.tsx create mode 100644 src-web/lib/data/charsets.ts create mode 100644 src-web/lib/data/connections.ts create mode 100644 src-web/lib/data/encodings.ts create mode 100644 src-web/lib/data/headerNames.ts create mode 100644 src-web/lib/data/mimetypes.ts diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 581fdf3c6bc66769da83b87a5966b38e21d666f7..6c0183daa796d0a6b0849971dad888d27a408e5b 100644 GIT binary patch delta 92 zcmdmUR&~!=RgTQ$ykZ8; diff --git a/src-web/components/HeaderEditor.tsx b/src-web/components/HeaderEditor.tsx new file mode 100644 index 00000000..793305ab --- /dev/null +++ b/src-web/components/HeaderEditor.tsx @@ -0,0 +1,52 @@ +import { charsets } from '../lib/data/charsets'; +import { connections } from '../lib/data/connections'; +import { encodings } from '../lib/data/encodings'; +import { headerNames } from '../lib/data/headerNames'; +import { mimeTypes } from '../lib/data/mimetypes'; +import type { HttpRequest } from '../lib/models'; +import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; +import type { PairEditorProps } from './core/PairEditor'; +import { PairEditor } from './core/PairEditor'; + +type Props = { + headers: HttpRequest['headers']; + onChange: (headers: HttpRequest['headers']) => void; +}; + +export function HeaderEditor({ headers, onChange }: Props) { + return ( + + ); +} + +const MIN_MATCH = 3; + +const headerOptionsMap: Record = { + 'content-type': mimeTypes, + accept: ['*/*', ...mimeTypes], + 'accept-encoding': encodings, + connection: connections, + 'accept-charset': charsets, +}; + +const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => { + const name = headerName.toLowerCase().trim(); + const options: GenericCompletionConfig['options'] = + headerOptionsMap[name]?.map((o, i) => ({ + label: o, + type: 'constant', + boost: 99 - i, // Max boost is 99 + })) ?? []; + return { minMatch: MIN_MATCH, options }; +}; + +const nameAutocomplete: PairEditorProps['nameAutocomplete'] = { + minMatch: MIN_MATCH, + options: headerNames.map((t, i) => ({ label: t, type: 'constant', boost: 99 - i })), +}; diff --git a/src-web/components/ParameterEditor.tsx b/src-web/components/ParameterEditor.tsx new file mode 100644 index 00000000..02feaff7 --- /dev/null +++ b/src-web/components/ParameterEditor.tsx @@ -0,0 +1,11 @@ +import type { HttpRequest } from '../lib/models'; +import { PairEditor } from './core/PairEditor'; + +type Props = { + parameters: { name: string; value: string }[]; + onChange: (headers: HttpRequest['headers']) => void; +}; + +export function ParametersEditor({ parameters, onChange }: Props) { + return ; +} diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index d5248e14..dc72ee04 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -1,5 +1,6 @@ import classnames from 'classnames'; import { useCallback, useMemo } from 'react'; +import { act } from 'react-dom/test-utils'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useKeyValue } from '../hooks/useKeyValue'; import { useUpdateRequest } from '../hooks/useUpdateRequest'; @@ -9,7 +10,9 @@ import { Editor } from './core/Editor'; import { PairEditor } from './core/PairEditor'; import type { TabItem } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs'; -import { GraphQLEditor } from './editors/GraphQLEditor'; +import { GraphQLEditor } from './GraphQLEditor'; +import { HeaderEditor } from './HeaderEditor'; +import { ParametersEditor } from './ParameterEditor'; import { UrlBar } from './UrlBar'; interface Props { @@ -67,12 +70,15 @@ export function RequestPane({ fullHeight, className }: Props) { label="Request body" > - + + null} /> + {activeRequest.bodyType === 'json' ? ( void; singleLine?: boolean; format?: (v: string) => string; - autocompleteOptions?: GenericCompletionOption[]; + autocomplete?: GenericCompletionConfig; } export function _Editor({ @@ -43,7 +43,7 @@ export function _Editor({ className, singleLine, format, - autocompleteOptions, + autocomplete, }: _EditorProps) { const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null); const wrapperRef = useRef(null); @@ -80,16 +80,16 @@ export function _Editor({ useEffect(() => { if (cm.current === null) return; const { view, languageCompartment } = cm.current; - const ext = getLanguageExtension({ contentType, useTemplating, autocompleteOptions }); + const ext = getLanguageExtension({ contentType, useTemplating, autocomplete }); view.dispatch({ effects: languageCompartment.reconfigure(ext) }); - }, [contentType, JSON.stringify(autocompleteOptions)]); + }, [contentType, autocomplete]); // Initialize the editor when ref mounts useEffect(() => { if (wrapperRef.current === null || cm.current !== null) return; try { const languageCompartment = new Compartment(); - const langExt = getLanguageExtension({ contentType, useTemplating, autocompleteOptions }); + const langExt = getLanguageExtension({ contentType, useTemplating, autocomplete }); const state = EditorState.create({ doc: `${defaultValue ?? ''}`, extensions: [ diff --git a/src-web/components/core/Editor/extensions.ts b/src-web/components/core/Editor/extensions.ts index a45943e7..f10272e0 100644 --- a/src-web/components/core/Editor/extensions.ts +++ b/src-web/components/core/Editor/extensions.ts @@ -34,6 +34,7 @@ import { import { tags as t } from '@lezer/highlight'; import { graphqlLanguageSupport } from 'cm6-graphql'; import type { GenericCompletionOption } from './genericCompletion'; +import type { EditorProps } from './index'; import { text } from './text/extension'; import { twig } from './twig/extension'; import { url } from './url/extension'; @@ -95,19 +96,15 @@ const syntaxExtensions: Record = { export function getLanguageExtension({ contentType, useTemplating = false, - autocompleteOptions, -}: { - contentType?: string; - useTemplating?: boolean; - autocompleteOptions?: GenericCompletionOption[]; -}) { + autocomplete, +}: Pick) { const justContentType = contentType?.split(';')[0] ?? contentType ?? ''; const base = syntaxExtensions[justContentType] ?? text(); if (!useTemplating) { return base ? base : []; } - return twig(base, autocompleteOptions); + return twig(base, autocomplete); } export const baseExtensions = [ diff --git a/src-web/components/core/Editor/genericCompletion.ts b/src-web/components/core/Editor/genericCompletion.ts index 9641b548..a181c6a0 100644 --- a/src-web/components/core/Editor/genericCompletion.ts +++ b/src-web/components/core/Editor/genericCompletion.ts @@ -3,17 +3,21 @@ import type { CompletionContext } from '@codemirror/autocomplete'; export interface GenericCompletionOption { label: string; type: 'constant' | 'variable'; + /** When given, should be a number from -99 to 99 that adjusts + * how this completion is ranked compared to other completions + * that match the input as well as this one. A negative number + * moves it down the list, a positive number moves it up. */ + boost?: number; } -export function genericCompletion({ - options, - minMatch = 1, -}: { - options: GenericCompletionOption[]; +export interface GenericCompletionConfig { minMatch?: number; -}) { + options: GenericCompletionOption[]; +} + +export function genericCompletion({ options, minMatch = 1 }: GenericCompletionConfig) { return function completions(context: CompletionContext) { - const toMatch = context.matchBefore(/^[\w:/]*/); + const toMatch = context.matchBefore(/^.*/); if (toMatch === null) return null; const matchedMinimumLength = toMatch.to - toMatch.from >= minMatch; diff --git a/src-web/components/core/Editor/twig/extension.ts b/src-web/components/core/Editor/twig/extension.ts index 98f9669e..8e7cfa1d 100644 --- a/src-web/components/core/Editor/twig/extension.ts +++ b/src-web/components/core/Editor/twig/extension.ts @@ -1,16 +1,16 @@ import { LanguageSupport, LRLanguage } from '@codemirror/language'; import { parseMixed } from '@lezer/common'; -import type { GenericCompletionOption } from '../genericCompletion'; +import type { GenericCompletionConfig } from '../genericCompletion'; import { genericCompletion } from '../genericCompletion'; import { placeholders } from '../widgets'; import { completions } from './completion'; import { parser as twigParser } from './twig'; -export function twig(base?: LanguageSupport, autocompleteOptions?: GenericCompletionOption[]) { +export function twig(base?: LanguageSupport, autocomplete?: GenericCompletionConfig) { const language = mixedOrPlainLanguage(base); const additionalCompletion = - autocompleteOptions && base - ? [language.data.of({ autocomplete: genericCompletion({ options: autocompleteOptions }) })] + autocomplete && base + ? [language.data.of({ autocomplete: genericCompletion(autocomplete) })] : []; const completion = language.data.of({ autocomplete: completions, diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx index 8f1d6866..8cc5d687 100644 --- a/src-web/components/core/Input.tsx +++ b/src-web/components/core/Input.tsx @@ -12,7 +12,7 @@ type Props = Omit, 'onChange' | 'onFocus'> & { containerClassName?: string; onChange?: (value: string) => void; onFocus?: () => void; - useEditor?: Pick; + useEditor?: Pick; defaultValue?: string; leftSlot?: ReactNode; rightSlot?: ReactNode; diff --git a/src-web/components/core/PairEditor.tsx b/src-web/components/core/PairEditor.tsx index f97da75d..bf862ce9 100644 --- a/src-web/components/core/PairEditor.tsx +++ b/src-web/components/core/PairEditor.tsx @@ -1,35 +1,39 @@ import classnames from 'classnames'; -import { memo, useEffect, useMemo, useState } from 'react'; -import type { GenericCompletionOption } from './Editor/genericCompletion'; +import { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import type { GenericCompletionConfig } from './Editor/genericCompletion'; import { IconButton } from './IconButton'; import { Input } from './Input'; import { VStack } from './Stacks'; -interface Props { +export type PairEditorProps = { pairs: Pair[]; onChange: (pairs: Pair[]) => void; className?: string; -} + namePlaceholder?: string; + valuePlaceholder?: string; + nameAutocomplete?: GenericCompletionConfig; + valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined; +}; -interface Pair { +type Pair = { name: string; value: string; -} +}; -interface PairContainer { +type PairContainer = { pair: Pair; id: string; -} +}; export const PairEditor = memo(function PairEditor({ pairs: originalPairs, + nameAutocomplete, + valueAutocomplete, + namePlaceholder, + valuePlaceholder, className, onChange, -}: Props) { - const newPairContainer = (): PairContainer => { - return { pair: { name: '', value: '' }, id: Math.random().toString() }; - }; - +}: PairEditorProps) { const [pairs, setPairs] = useState(() => { // Remove empty headers on initial render const nonEmpty = originalPairs.filter((h) => !(h.name === '' && h.value === '')); @@ -37,17 +41,20 @@ export const PairEditor = memo(function PairEditor({ return [...pairs, newPairContainer()]; }); - const setPairsAndSave = (fn: (pairs: PairContainer[]) => PairContainer[]) => { - setPairs((oldPairs) => { - const pairs = fn(oldPairs).map((p) => p.pair); - onChange(pairs); - return fn(oldPairs); - }); - }; + const setPairsAndSave = useCallback( + (fn: (pairs: PairContainer[]) => PairContainer[]) => { + setPairs((oldPairs) => { + const pairs = fn(oldPairs).map((p) => p.pair); + onChange(pairs); + return fn(oldPairs); + }); + }, + [onChange], + ); - const handleChangeHeader = (pair: PairContainer) => { + const handleChangeHeader = useCallback((pair: PairContainer) => { setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))); - }; + }, []); // Ensure there's always at least one pair useEffect(() => { @@ -56,9 +63,19 @@ export const PairEditor = memo(function PairEditor({ } }, [pairs]); - const handleDelete = (pair: PairContainer) => { + const handleDelete = useCallback((pair: PairContainer) => { setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id)); - }; + }, []); + + const handleFocus = useCallback( + (pair: PairContainer) => { + const isLast = pair.id === pairs[pairs.length - 1]?.id; + if (isLast) { + setPairs((pairs) => [...pairs, newPairContainer()]); + } + }, + [pairs], + ); return (
@@ -71,11 +88,11 @@ export const PairEditor = memo(function PairEditor({ pairContainer={p} isLast={isLast} onChange={handleChangeHeader} - onFocus={() => { - if (isLast) { - setPairs((pairs) => [...pairs, newPairContainer()]); - } - }} + nameAutocomplete={nameAutocomplete} + valueAutocomplete={valueAutocomplete} + namePlaceholder={namePlaceholder} + valuePlaceholder={valuePlaceholder} + onFocus={handleFocus} onDelete={isLast ? undefined : handleDelete} /> ); @@ -85,46 +102,65 @@ export const PairEditor = memo(function PairEditor({ ); }); +type FormRowProps = { + pairContainer: PairContainer; + onChange: (pair: PairContainer) => void; + onDelete?: (pair: PairContainer) => void; + onFocus?: (pair: PairContainer) => void; + isLast?: boolean; +} & Pick< + PairEditorProps, + 'nameAutocomplete' | 'valueAutocomplete' | 'namePlaceholder' | 'valuePlaceholder' +>; + const FormRow = memo(function FormRow({ pairContainer, onChange, onDelete, onFocus, isLast, -}: { - pairContainer: PairContainer; - onChange: (pair: PairContainer) => void; - onDelete?: (pair: PairContainer) => void; - onFocus?: () => void; - isLast?: boolean; -}) { + nameAutocomplete, + valueAutocomplete, + namePlaceholder, + valuePlaceholder, +}: FormRowProps) { const { id } = pairContainer; - const valueOptions = useMemo(() => { - if (pairContainer.pair.name.toLowerCase() === 'content-type') { - return [ - { label: 'application/json', type: 'constant' }, - { label: 'text/xml', type: 'constant' }, - { label: 'text/html', type: 'constant' }, - ]; - } - return undefined; - }, [pairContainer.pair.value]); + + const handleChangeName = useMemo( + () => (name: string) => onChange({ id, pair: { name, value: pairContainer.pair.value } }), + [onChange, pairContainer.pair.value], + ); + + const handleChangeValue = useMemo( + () => (value: string) => onChange({ id, pair: { value, name: pairContainer.pair.name } }), + [onChange, pairContainer.pair.name], + ); + + const nameEditorConfig = useMemo( + () => ({ useTemplating: true, autocomplete: nameAutocomplete }), + [nameAutocomplete], + ); + + const valueEditorConfig = useMemo( + () => ({ useTemplating: true, autocomplete: valueAutocomplete?.(pairContainer.pair.name) }), + [valueAutocomplete, pairContainer.pair.name], + ); + + const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]); + const handleDelete = useCallback(() => onDelete?.(pairContainer), [onDelete, pairContainer]); return ( -
+
onChange({ id, pair: { name, value: pairContainer.pair.value } })} - onFocus={onFocus} - placeholder={isLast ? 'new name' : 'name'} - useEditor={{ - useTemplating: true, - autocompleteOptions: [{ label: 'Content-Type', type: 'constant' }], - }} + onChange={handleChangeName} + onFocus={handleFocus} + placeholder={namePlaceholder ?? 'name'} + useEditor={nameEditorConfig} /> onChange({ id, pair: { name: pairContainer.pair.name, value } })} - onFocus={onFocus} - placeholder={isLast ? 'new value' : 'value'} - useEditor={{ useTemplating: true, autocompleteOptions: valueOptions }} + onChange={handleChangeValue} + onFocus={handleFocus} + placeholder={valuePlaceholder ?? 'value'} + useEditor={valueEditorConfig} /> {onDelete ? ( onDelete(pairContainer)} + onClick={handleDelete} tabIndex={-1} className={classnames('opacity-0 group-hover:opacity-100')} /> @@ -151,3 +187,7 @@ const FormRow = memo(function FormRow({
); }); + +const newPairContainer = (): PairContainer => { + return { pair: { name: '', value: '' }, id: Math.random().toString() }; +}; diff --git a/src-web/lib/data/charsets.ts b/src-web/lib/data/charsets.ts new file mode 100644 index 00000000..52e738db --- /dev/null +++ b/src-web/lib/data/charsets.ts @@ -0,0 +1,121 @@ +export const charsets = [ + 'utf-8', + 'us-ascii', + '950', + 'ASMO-708', + 'CP1026', + 'CP870', + 'DOS-720', + 'DOS-862', + 'EUC-CN', + 'IBM437', + 'Johab', + 'Windows-1252', + 'X-EBCDIC-Spain', + 'big5', + 'cp866', + 'csISO2022JP', + 'ebcdic-cp-us', + 'euc-kr', + 'gb2312', + 'hz-gb-2312', + 'ibm737', + 'ibm775', + 'ibm850', + 'ibm852', + 'ibm857', + 'ibm861', + 'ibm869', + 'iso-2022-jp', + 'iso-2022-jp', + 'iso-2022-kr', + 'iso-8859-1', + 'iso-8859-15', + 'iso-8859-2', + 'iso-8859-3', + 'iso-8859-4', + 'iso-8859-5', + 'iso-8859-6', + 'iso-8859-7', + 'iso-8859-8', + 'iso-8859-8-i', + 'iso-8859-9', + 'koi8-r', + 'koi8-u', + 'ks_c_5601-1987', + 'macintosh', + 'shift_jis', + 'unicode', + 'unicodeFFFE', + 'utf-7', + 'windows-1250', + 'windows-1251', + 'windows-1253', + 'windows-1254', + 'windows-1255', + 'windows-1256', + 'windows-1257', + 'windows-1258', + 'windows-874', + 'x-Chinese-CNS', + 'x-Chinese-Eten', + 'x-EBCDIC-Arabic', + 'x-EBCDIC-CyrillicRussian', + 'x-EBCDIC-CyrillicSerbianBulgarian', + 'x-EBCDIC-DenmarkNorway', + 'x-EBCDIC-FinlandSweden', + 'x-EBCDIC-Germany', + 'x-EBCDIC-Greek', + 'x-EBCDIC-GreekModern', + 'x-EBCDIC-Hebrew', + 'x-EBCDIC-Icelandic', + 'x-EBCDIC-Italy', + 'x-EBCDIC-JapaneseAndJapaneseLatin', + 'x-EBCDIC-JapaneseAndKana', + 'x-EBCDIC-JapaneseAndUSCanada', + 'x-EBCDIC-JapaneseKatakana', + 'x-EBCDIC-KoreanAndKoreanExtended', + 'x-EBCDIC-KoreanExtended', + 'x-EBCDIC-SimplifiedChinese', + 'x-EBCDIC-Thai', + 'x-EBCDIC-TraditionalChinese', + 'x-EBCDIC-Turkish', + 'x-EBCDIC-UK', + 'x-Europa', + 'x-IA5', + 'x-IA5-German', + 'x-IA5-Norwegian', + 'x-IA5-Swedish', + 'x-ebcdic-cp-us-euro', + 'x-ebcdic-denmarknorway-euro', + 'x-ebcdic-finlandsweden-euro', + 'x-ebcdic-finlandsweden-euro', + 'x-ebcdic-france-euro', + 'x-ebcdic-germany-euro', + 'x-ebcdic-icelandic-euro', + 'x-ebcdic-international-euro', + 'x-ebcdic-italy-euro', + 'x-ebcdic-spain-euro', + 'x-ebcdic-uk-euro', + 'x-euc-jp', + 'x-iscii-as', + 'x-iscii-be', + 'x-iscii-de', + 'x-iscii-gu', + 'x-iscii-ka', + 'x-iscii-ma', + 'x-iscii-or', + 'x-iscii-pa', + 'x-iscii-ta', + 'x-iscii-te', + 'x-mac-arabic', + 'x-mac-ce', + 'x-mac-chinesesimp', + 'x-mac-cyrillic', + 'x-mac-greek', + 'x-mac-hebrew', + 'x-mac-icelandic', + 'x-mac-japanese', + 'x-mac-korean', + 'x-mac-turkish', +]; diff --git a/src-web/lib/data/connections.ts b/src-web/lib/data/connections.ts new file mode 100644 index 00000000..c117e291 --- /dev/null +++ b/src-web/lib/data/connections.ts @@ -0,0 +1 @@ +export const connections = ['close', 'keep-alive']; diff --git a/src-web/lib/data/encodings.ts b/src-web/lib/data/encodings.ts new file mode 100644 index 00000000..0c18fb7d --- /dev/null +++ b/src-web/lib/data/encodings.ts @@ -0,0 +1 @@ +export const encodings = ['*', 'gzip', 'compress', 'deflate', 'br', 'identity']; diff --git a/src-web/lib/data/headerNames.ts b/src-web/lib/data/headerNames.ts new file mode 100644 index 00000000..5edf6a53 --- /dev/null +++ b/src-web/lib/data/headerNames.ts @@ -0,0 +1,35 @@ +export const headerNames = [ + 'Content-Type', + 'Content-Length', + 'Accept', + 'Accept-Charset', + 'Accept-Encoding', + 'Accept-Language', + 'Accept-Datetime', + 'Authorization', + 'Cache-Control', + 'Cookie', + 'Connection', + 'Content-MD5', + 'Date', + 'Expect', + 'Forwarded', + 'From', + 'Host', + 'If-Match', + 'If-Modified-Since', + 'If-None-Match', + 'If-Range', + 'If-Unmodified-Since', + 'Max-Forwards', + 'Origin', + 'Pragma', + 'Proxy-Authorization', + 'Range', + 'Referer', + 'TE', + 'User-Agent', + 'Upgrade', + 'Via', + 'Warning', +]; diff --git a/src-web/lib/data/mimetypes.ts b/src-web/lib/data/mimetypes.ts new file mode 100644 index 00000000..bd4a8f4d --- /dev/null +++ b/src-web/lib/data/mimetypes.ts @@ -0,0 +1,208 @@ +export const mimeTypes = [ + 'application/json', + 'application/xml', + 'application/x-www-form-urlencoded', + 'multipart/form-data', + 'multipart/byteranges', + 'application/octet-stream', + 'text/plain', + 'application/javascript', + 'application/pdf', + 'text/html', + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'text/css', + 'application/x-pkcs12', + 'application/xhtml+xml', + 'application/andrew-inset', + 'application/applixware', + 'application/atom+xml', + 'application/atomcat+xml', + 'application/atomsvc+xml', + 'application/bdoc', + 'application/cu-seeme', + 'application/davmount+xml', + 'application/docbook+xml', + 'application/dssc+xml', + 'application/ecmascript', + 'application/epub+zip', + 'application/exi', + 'application/font-tdpfr', + 'application/font-woff', + 'application/font-woff2', + 'application/geo+json', + 'application/graphql', + 'application/java-serialized-object', + 'application/json5', + 'application/jsonml+json', + 'application/ld+json', + 'application/lost+xml', + 'application/manifest+json', + 'application/mp4', + 'application/msword', + 'application/mxf', + 'application/oda', + 'application/ogg', + 'application/pgp-encrypted', + 'application/pgp-signature', + 'application/pics-rules', + 'application/pkcs10', + 'application/pkcs7-mime', + 'application/pkcs7-signature', + 'application/pkcs8', + 'application/postscript', + 'application/pskc+xml', + 'application/resource-lists+xml', + 'application/resource-lists-diff+xml', + 'application/rls-services+xml', + 'application/rsd+xml', + 'application/rss+xml', + 'application/rtf', + 'application/sdp', + 'application/shf+xml', + 'application/timestamped-data', + 'application/vnd.android.package-archive', + 'application/vnd.api+json', + 'application/vnd.apple.installer+xml', + 'application/vnd.apple.mpegurl', + 'application/vnd.apple.pkpass', + 'application/vnd.bmi', + 'application/vnd.curl.car', + 'application/vnd.curl.pcurl', + 'application/vnd.dna', + 'application/vnd.google-apps.document', + 'application/vnd.google-apps.presentation', + 'application/vnd.google-apps.spreadsheet', + 'application/vnd.hal+xml', + 'application/vnd.handheld-entertainment+xml', + 'application/vnd.macports.portpkg', + 'application/vnd.unity', + 'application/vnd.zul', + 'application/widget', + 'application/wsdl+xml', + 'application/x-7z-compressed', + 'application/x-ace-compressed', + 'application/x-bittorrent', + 'application/x-bzip', + 'application/x-bzip2', + 'application/x-cfs-compressed', + 'application/x-chrome-extension', + 'application/x-cocoa', + 'application/x-envoy', + 'application/x-eva', + 'font/opentype', + 'application/x-gca-compressed', + 'application/x-gtar', + 'application/x-hdf', + 'application/x-httpd-php', + 'application/x-install-instructions', + 'application/x-latex', + 'application/x-lua-bytecode', + 'application/x-lzh-compressed', + 'application/x-ms-application', + 'application/x-ms-shortcut', + 'application/x-ndjson', + 'application/x-perl', + 'application/x-pkcs7-certificates', + 'application/x-pkcs7-certreqresp', + 'application/x-rar-compressed', + 'application/x-sh', + 'application/x-sql', + 'application/x-subrip', + 'application/x-t3vm-image', + 'application/x-tads', + 'application/x-tar', + 'application/x-tcl', + 'application/x-tex', + 'application/x-x509-ca-cert', + 'application/xop+xml', + 'application/xslt+xml', + 'application/zip', + 'audio/3gpp', + 'audio/adpcm', + 'audio/basic', + 'audio/midi', + 'audio/mpeg', + 'audio/mp4', + 'audio/ogg', + 'audio/silk', + 'audio/wave', + 'audio/webm', + 'audio/x-aac', + 'audio/x-aiff', + 'audio/x-caf', + 'audio/x-flac', + 'audio/xm', + 'image/bmp', + 'image/cgm', + 'image/sgi', + 'image/svg+xml', + 'image/tiff', + 'image/x-3ds', + 'image/x-freehand', + 'image/x-icon', + 'image/x-jng', + 'image/x-mrsid-image', + 'image/x-pcx', + 'image/x-pict', + 'image/x-rgb', + 'image/x-tga', + 'message/rfc822', + 'text/cache-manifest', + 'text/calendar', + 'text/coffeescript', + 'text/csv', + 'text/hjson', + 'text/jade', + 'text/jsx', + 'text/less', + 'text/mathml', + 'text/n3', + 'text/richtext', + 'text/sgml', + 'text/slim', + 'text/stylus', + 'text/tab-separated-values', + 'text/uri-list', + 'text/vcard', + 'text/vnd.curl', + 'text/vnd.fly', + 'text/vtt', + 'text/x-asm', + 'text/x-c', + 'text/x-component', + 'text/x-fortran', + 'text/x-handlebars-template', + 'text/x-java-source', + 'text/x-lua', + 'text/x-markdown', + 'text/x-nfo', + 'text/x-opml', + 'text/x-pascal', + 'text/x-processing', + 'text/x-sass', + 'text/x-scss', + 'text/x-vcalendar', + 'text/xml', + 'text/yaml', + 'video/3gpp', + 'video/3gpp2', + 'video/h261', + 'video/h263', + 'video/h264', + 'video/jpeg', + 'video/jpm', + 'video/mj2', + 'video/mp2t', + 'video/mp4', + 'video/mpeg', + 'video/ogg', + 'video/quicktime', + 'video/webm', + 'video/x-f4v', + 'video/x-fli', + 'video/x-flv', + 'video/x-m4v', +];