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",
        },
      });
    })(),
  );
});