Spaces:
Sleeping
Sleeping
File size: 5,524 Bytes
9aad4d7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | // PRISM Markdown for Agents — content-negotiation service worker
//
// GitHub Pages does not run edge functions, so server-side Accept-header
// negotiation is not available. This service worker implements the same
// behaviour in the browser: when a request prefers text/markdown, return
// the pre-built .md companion (if one exists) with the right Content-Type
// and the x-markdown-tokens header. HTML remains the default.
//
// The service worker MUST be served from the same origin. The HTML pages
// register it via <script>navigator.serviceWorker.register('/sw.js')</script>.
//
// Scope: the SW controls the origin root. The demo.html / paper.html
// pages that opt in register it on first load; only those clients get
// the markdown negotiation. Non-browser agents (curl, httpx, fetch()
// without SW support) fall back to HTML at /.md URLs.
const VERSION = "prism-md-v1";
const MD_CACHE = `${VERSION}-md`;
const HTML_CACHE = `${VERSION}-html`;
const MD_MANIFEST = new Set([
"/",
"/index.md",
"/demo",
"/demo.md",
"/index.html",
"/demo/index.html",
]);
// Crude token estimate: ~0.75 tokens per whitespace-separated word is a
// reasonable approximation for English prose. The Cloudflare x-markdown-tokens
// header is informational, so a small inaccuracy is acceptable; clients that
// need exact tokenization can re-count on their side.
function estimateTokens(text) {
if (!text) return 0;
const words = text.trim().split(/\s+/).length;
return Math.max(1, Math.round(words * 0.75));
}
function prefersMarkdown(request) {
const accept = request.headers.get("accept") || "";
if (!accept) return false;
// Parse the Accept header. text/markdown wins if it is explicitly
// listed with any q-value, OR if it appears with a higher q-value
// than text/html.
const parts = accept
.split(",")
.map((s) => s.trim().toLowerCase())
.filter(Boolean)
.map((s) => {
const [type, ...params] = s.split(";").map((x) => x.trim());
const qParam = params.find((p) => p.startsWith("q="));
const q = qParam ? parseFloat(qParam.slice(2)) : 1.0;
return { type, q: Number.isFinite(q) ? q : 1.0 };
});
const md = parts.find((p) => p.type === "text/markdown");
const html = parts.find(
(p) => p.type === "text/html" || p.type === "application/xhtml+xml",
);
if (!md) return false;
if (!html) return true;
return md.q > html.q;
}
// Map a request URL to the .md companion. For directory-style paths
// ("/" and "/demo/"), the companion is "<path>index.md" or
// "<path>.md" respectively. For explicit .html files, drop the .html.
function mdCompanionFor(url) {
const path = url.pathname;
if (path.endsWith(".md")) return path; // already markdown
if (path.endsWith(".html")) {
return path.replace(/\.html$/, ".md");
}
if (path === "/" || path.endsWith("/")) {
return path.replace(/\/?$/, "/index.md");
}
return `${path}.md`;
}
function isNavigation(request) {
return (
request.mode === "navigate" ||
(request.method === "GET" &&
request.headers.get("sec-fetch-mode") === "navigate")
);
}
self.addEventListener("install", (event) => {
// Pre-cache the markdown companions so the first request is instant.
event.waitUntil(
caches
.open(MD_CACHE)
.then((cache) =>
cache.addAll(
["/index.md", "/demo.md"].filter((u) => MD_MANIFEST.has(u)),
),
),
);
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(
keys.filter((k) => !k.startsWith(VERSION)).map((k) => caches.delete(k)),
);
await self.clients.claim();
})(),
);
});
self.addEventListener("fetch", (event) => {
const request = event.request;
if (request.method !== "GET") return;
const url = new URL(request.url);
if (url.origin !== self.location.origin) return;
// Only negotiate for navigations and direct page fetches. Static
// assets (CSS, JS, images, fonts) always go to the network unchanged.
const isPageRequest =
isNavigation(request) ||
(request.headers.get("accept") || "").includes("text/markdown");
if (!isPageRequest) return;
if (!prefersMarkdown(request)) return;
const companion = mdCompanionFor(url);
if (!MD_MANIFEST.has(companion) && !MD_MANIFEST.has(url.pathname)) return;
event.respondWith(
(async () => {
const cache = await caches.open(MD_CACHE);
let response = await cache.match(companion);
if (!response) {
try {
const networkResponse = await fetch(companion, { cache: "no-store" });
if (!networkResponse.ok) {
// No companion available — fall through to the HTML.
return fetch(request);
}
await cache.put(companion, networkResponse.clone());
response = networkResponse;
} catch (err) {
// Offline and not cached — fall through.
return fetch(request);
}
}
const body = await response.text();
const tokens = estimateTokens(body);
return new Response(body, {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "text/markdown; charset=utf-8",
Vary: "Accept",
"x-markdown-tokens": String(tokens),
"Content-Signal": "ai-train=no, search=yes, ai-input=no",
"Cache-Control": "public, max-age=300",
},
});
})(),
);
});
|