Files

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'
}