Ctrl K
ComunidadGuías…

docs/src/lib/content-negotiation.ts

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 };
}