mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 13:17:00 +04:00
67 lines
2.2 KiB
TypeScript
67 lines
2.2 KiB
TypeScript
// Accept negotiation between text/html and text/markdown (RFC 9110 q-values).
|
|
// A tie goes to markdown only when the client named text/markdown explicitly,
|
|
// so `Accept: */*` from a browser still gets html; 'none' means answer 406.
|
|
|
|
type MediaRange = {
|
|
type: string
|
|
subtype: string
|
|
q: number
|
|
}
|
|
|
|
export type Format = 'html' | 'markdown' | 'none'
|
|
|
|
function parseAccept(header: string): MediaRange[] {
|
|
return header
|
|
.split(',')
|
|
.map((part): MediaRange => {
|
|
const [range = '', ...params] = part.trim().split(';')
|
|
const [type = '', subtype = ''] = range.trim().toLowerCase().split('/')
|
|
let q = 1
|
|
for (const param of params) {
|
|
const [key, value] = param.split('=')
|
|
if (key?.trim().toLowerCase() === 'q') {
|
|
const parsed = Number.parseFloat(value ?? '')
|
|
// a present but unparsable q fails closed
|
|
q = Number.isNaN(parsed) ? 0 : Math.min(Math.max(parsed, 0), 1)
|
|
}
|
|
}
|
|
return { type, subtype, q }
|
|
})
|
|
.filter((range) => range.type !== '' && range.subtype !== '')
|
|
}
|
|
|
|
// The q of the most specific matching range: exact type beats `text/*` beats `*/*`.
|
|
function match(ranges: MediaRange[], type: string, subtype: string) {
|
|
let q = 0
|
|
let specificity = -1
|
|
let explicit = false
|
|
for (const range of ranges) {
|
|
let rangeSpecificity: number
|
|
if (range.type === type && range.subtype === subtype) rangeSpecificity = 2
|
|
else if (range.type === type && range.subtype === '*') rangeSpecificity = 1
|
|
else if (range.type === '*' && range.subtype === '*') rangeSpecificity = 0
|
|
else continue
|
|
if (rangeSpecificity > specificity) {
|
|
specificity = rangeSpecificity
|
|
q = range.q
|
|
explicit = rangeSpecificity === 2
|
|
}
|
|
}
|
|
return { q, explicit }
|
|
}
|
|
|
|
export function preferredFormat(header: string | null): Format {
|
|
if (!header || header.trim() === '') return 'html'
|
|
const ranges = parseAccept(header)
|
|
if (ranges.length === 0) return 'html'
|
|
|
|
const html = match(ranges, 'text', 'html')
|
|
const markdown = match(ranges, 'text', 'markdown')
|
|
|
|
if (markdown.q > html.q) return 'markdown'
|
|
if (markdown.q === html.q && markdown.q > 0 && markdown.explicit) return 'markdown'
|
|
if (html.q > 0) return 'html'
|
|
if (markdown.q > 0) return 'markdown'
|
|
return 'none'
|
|
}
|