docs/src/lib/content-negotiation.ts
Rama master · réplica de lectura del repositorio original.
const PRODUCES = ["text/html", "text/markdown"] as const;
type ProducedType = (typeof PRODUCES)[number];
interface AcceptEntry {
position: number;
q: number;
specificity: number;
type: string;
}
function parseAccept(header: string): AcceptEntry[] {
return header
.split(",")
.map((raw, position) => {
const [rawType = "", ...rawParameters] = raw.split(";");
const type = rawType.trim().toLowerCase();
let q = 1;
for (const rawParameter of rawParameters) {
const [rawName = "", rawValue = ""] = rawParameter.split("=", 2);
if (rawName.trim().toLowerCase() !== "q") continue;
const value = rawValue.trim();
q = /^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/.test(value) ? Number(value) : 0;
}
return {
position,
q,
specificity: type === "*/*" ? 0 : type.endsWith("/*") ? 1 : 2,
type,
};
})
.filter(entry => entry.type.length > 0);
}
function matches(entry: AcceptEntry, candidate: ProducedType): boolean {
if (entry.type === "*/*") return true;
if (entry.type.endsWith("/*")) return candidate.startsWith(entry.type.slice(0, -1));
return entry.type === candidate;
}
/** Selects an HTTP representation according to RFC 9110 section 12.5.1. */
export function preferredType(header: string | null): ProducedType | null {
if (!header) return PRODUCES[0];
const entries = parseAccept(header);
if (entries.length === 0) return PRODUCES[0];
let best: { position: number; q: number; specificity: number; type: ProducedType } | null = null;
for (const candidate of PRODUCES) {
let matched: AcceptEntry | null = null;
for (const entry of entries) {
if (!matches(entry, candidate)) continue;
if (
matched === null ||
entry.specificity > matched.specificity ||
(entry.specificity === matched.specificity && entry.position < matched.position)
) {
matched = entry;
}
}
if (!matched || matched.q <= 0) continue;
if (
best === null ||
matched.q > best.q ||
(matched.q === best.q && matched.specificity > best.specificity) ||
(matched.q === best.q && matched.specificity === best.specificity && matched.position < best.position)
) {
best = {
position: matched.position,
q: matched.q,
specificity: matched.specificity,
type: candidate,
};
}
}
return best?.type ?? null;
}
export function appendVary(headers: Headers, ...names: string[]): void {
const existing = (headers.get("Vary") ?? "")
.split(",")
.map(value => value.trim())
.filter(Boolean);
const known = new Set(existing.map(value => value.toLowerCase()));
for (const name of names) {
if (!known.has(name.toLowerCase())) {
existing.push(name);
known.add(name.toLowerCase());
}
}
headers.set("Vary", existing.join(", "));
}
export function markdownProxyTarget(requestUrl: string | URL, headers: HeadersInit) {
const originalUrl = new URL(requestUrl);
const rewriteUrl = new URL(originalUrl);
const originalPath = originalUrl.pathname;
const originalSearch = originalUrl.search;
const requestHeaders = new Headers(headers);
rewriteUrl.pathname = "/api/agent/markdown";
rewriteUrl.search = "";
rewriteUrl.searchParams.set("path", originalPath);
if (originalSearch) rewriteUrl.searchParams.set("query", originalSearch);
requestHeaders.set("X-Rybbit-Markdown-Path", originalPath);
requestHeaders.set("X-Rybbit-Markdown-Query", originalSearch);
return { rewriteUrl, requestHeaders };
}