jekyll-agent-markdown 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -1
- data/README.md +231 -42
- data/docs/deployment.md +138 -0
- data/examples/cloudflare/src/worker.js +254 -0
- data/examples/cloudflare/wrangler.toml +8 -0
- data/examples/netlify/netlify/edge-functions/markdown-negotiation.ts +281 -0
- data/examples/netlify/netlify.toml +6 -0
- data/examples/nginx/negotiation.js +267 -0
- data/examples/nginx/nginx.conf +40 -0
- data/lib/jekyll/agent_markdown/agent_markdown_link_tag.rb +20 -0
- data/lib/jekyll/agent_markdown/author_metadata.rb +28 -0
- data/lib/jekyll/agent_markdown/collection_validator.rb +73 -0
- data/lib/jekyll/agent_markdown/configuration.rb +67 -6
- data/lib/jekyll/agent_markdown/date_metadata.rb +2 -8
- data/lib/jekyll/agent_markdown/document_exporter.rb +113 -0
- data/lib/jekyll/agent_markdown/document_header.rb +59 -0
- data/lib/jekyll/agent_markdown/document_settings.rb +170 -0
- data/lib/jekyll/agent_markdown/exported_document.rb +17 -0
- data/lib/jekyll/agent_markdown/generator.rb +78 -70
- data/lib/jekyll/agent_markdown/llms_document_index.rb +125 -0
- data/lib/jekyll/agent_markdown/llms_document_ordering.rb +40 -0
- data/lib/jekyll/agent_markdown/llms_full_renderer.rb +72 -0
- data/lib/jekyll/agent_markdown/llms_headings.rb +10 -15
- data/lib/jekyll/agent_markdown/llms_index_renderer.rb +102 -0
- data/lib/jekyll/agent_markdown/llms_text.rb +30 -0
- data/lib/jekyll/agent_markdown/metadata_footer.rb +26 -0
- data/lib/jekyll/agent_markdown/source_documents.rb +66 -0
- data/lib/jekyll/agent_markdown/version.rb +1 -1
- data/lib/jekyll-agent-markdown.rb +3 -0
- metadata +25 -4
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
const HTML = {
|
|
2
|
+
name: "html",
|
|
3
|
+
contentType: "text/html; charset=utf-8",
|
|
4
|
+
type: "text",
|
|
5
|
+
subtype: "html",
|
|
6
|
+
parameters: {charset: "utf-8"}
|
|
7
|
+
};
|
|
8
|
+
const MARKDOWN = {
|
|
9
|
+
name: "markdown",
|
|
10
|
+
contentType: "text/markdown; charset=utf-8",
|
|
11
|
+
type: "text",
|
|
12
|
+
subtype: "markdown",
|
|
13
|
+
parameters: {charset: "utf-8"}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
async fetch(request, env) {
|
|
18
|
+
const url = new URL(request.url);
|
|
19
|
+
const explicitMarkdown = url.pathname.endsWith(".md");
|
|
20
|
+
|
|
21
|
+
if (!isPagePath(url.pathname)) return env.ASSETS.fetch(request);
|
|
22
|
+
|
|
23
|
+
const preference = explicitMarkdown
|
|
24
|
+
? {selected: "markdown", htmlAcceptable: false}
|
|
25
|
+
: negotiate(request.headers.get("Accept"));
|
|
26
|
+
if (!preference.selected) return notAcceptable();
|
|
27
|
+
|
|
28
|
+
const paths = variantPaths(url.pathname, explicitMarkdown);
|
|
29
|
+
const {response, served} = await fetchVariant(request, env, url, paths, preference);
|
|
30
|
+
|
|
31
|
+
return decorate(response, served, paths, !explicitMarkdown);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// A negotiated Markdown variant is not guaranteed to exist: the plugin exports
|
|
36
|
+
// posts by default, so most page URLs have no .md sibling until the site opts
|
|
37
|
+
// pages or collections in. Fall back to HTML when the client accepts it rather
|
|
38
|
+
// than turning a page the host could serve into a 404.
|
|
39
|
+
async function fetchVariant(request, env, url, paths, preference) {
|
|
40
|
+
const response = await fetchAsset(request, env, url, paths[preference.selected]);
|
|
41
|
+
const missing = response.status === 404;
|
|
42
|
+
if (preference.selected !== "markdown" || !missing || !preference.htmlAcceptable) {
|
|
43
|
+
return {response, served: preference.selected};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const fallback = await fetchAsset(request, env, url, paths.html);
|
|
47
|
+
if (fallback.status === 404) return {response, served: "markdown"};
|
|
48
|
+
|
|
49
|
+
return {response: fallback, served: "html"};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fetchAsset(request, env, url, pathname) {
|
|
53
|
+
const assetUrl = new URL(url);
|
|
54
|
+
assetUrl.pathname = pathname;
|
|
55
|
+
return env.ASSETS.fetch(new Request(assetUrl, request));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function negotiate(accept) {
|
|
59
|
+
if (!accept || !accept.trim()) return {selected: "html", htmlAcceptable: true};
|
|
60
|
+
|
|
61
|
+
const ranges = parseAccept(accept);
|
|
62
|
+
const html = qualityFor(HTML, ranges);
|
|
63
|
+
const markdown = qualityFor(MARKDOWN, ranges);
|
|
64
|
+
const htmlAcceptable = html.quality > 0;
|
|
65
|
+
|
|
66
|
+
if (html.quality <= 0 && markdown.quality <= 0) return {selected: null, htmlAcceptable};
|
|
67
|
+
if (markdown.quality > html.quality) return {selected: "markdown", htmlAcceptable};
|
|
68
|
+
if (html.quality > markdown.quality) return {selected: "html", htmlAcceptable};
|
|
69
|
+
|
|
70
|
+
const explicitMarkdown = ranges.some((range) =>
|
|
71
|
+
range.type === "text" &&
|
|
72
|
+
range.subtype === "markdown" &&
|
|
73
|
+
range.quality > 0 &&
|
|
74
|
+
specificity(range, MARKDOWN) !== null
|
|
75
|
+
);
|
|
76
|
+
return {selected: explicitMarkdown ? "markdown" : "html", htmlAcceptable};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function qualityFor(representation, ranges) {
|
|
80
|
+
let bestSpecificity = -1;
|
|
81
|
+
let quality = 0;
|
|
82
|
+
|
|
83
|
+
for (const range of ranges) {
|
|
84
|
+
const rangeSpecificity = specificity(range, representation);
|
|
85
|
+
if (rangeSpecificity === null || rangeSpecificity < bestSpecificity) continue;
|
|
86
|
+
|
|
87
|
+
if (rangeSpecificity > bestSpecificity) {
|
|
88
|
+
bestSpecificity = rangeSpecificity;
|
|
89
|
+
quality = range.quality;
|
|
90
|
+
} else {
|
|
91
|
+
quality = Math.max(quality, range.quality);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {quality, specificity: bestSpecificity};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function specificity(range, representation) {
|
|
99
|
+
if (range.type === "*" && range.subtype !== "*") return null;
|
|
100
|
+
if (range.type !== "*" && range.type !== representation.type) return null;
|
|
101
|
+
if (range.subtype !== "*" && range.subtype !== representation.subtype) return null;
|
|
102
|
+
|
|
103
|
+
for (const [name, value] of Object.entries(range.parameters)) {
|
|
104
|
+
if (representation.parameters[name] !== value) return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const mediaSpecificity = range.type === "*" ? 0 : range.subtype === "*" ? 1 : 2;
|
|
108
|
+
return (mediaSpecificity * 100) + Object.keys(range.parameters).length;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseAccept(accept) {
|
|
112
|
+
return splitQuoted(accept, ",").map(parseRange).filter(Boolean);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseRange(source) {
|
|
116
|
+
const parts = splitQuoted(source, ";");
|
|
117
|
+
const media = parts.shift().trim().toLowerCase();
|
|
118
|
+
const match = media.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
119
|
+
if (!match) return null;
|
|
120
|
+
|
|
121
|
+
const range = {type: match[1], subtype: match[2], quality: 1, parameters: {}};
|
|
122
|
+
for (const sourceParameter of parts) {
|
|
123
|
+
const separator = sourceParameter.indexOf("=");
|
|
124
|
+
if (separator < 1) {
|
|
125
|
+
if (sourceParameter.trim().toLowerCase() === "q") range.quality = 0;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const name = sourceParameter.slice(0, separator).trim().toLowerCase();
|
|
130
|
+
const rawValue = sourceParameter.slice(separator + 1).trim();
|
|
131
|
+
if (name === "q") {
|
|
132
|
+
range.quality = parseQuality(rawValue.toLowerCase());
|
|
133
|
+
} else {
|
|
134
|
+
range.parameters[name] = unquote(rawValue).toLowerCase();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return range;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseQuality(value) {
|
|
142
|
+
return /^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/.test(value) ? Number(value) : 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function splitQuoted(source, separator) {
|
|
146
|
+
const parts = [];
|
|
147
|
+
let current = "";
|
|
148
|
+
let quoted = false;
|
|
149
|
+
let escaped = false;
|
|
150
|
+
|
|
151
|
+
for (const character of source) {
|
|
152
|
+
if (escaped) {
|
|
153
|
+
current += character;
|
|
154
|
+
escaped = false;
|
|
155
|
+
} else if (character === "\\" && quoted) {
|
|
156
|
+
current += character;
|
|
157
|
+
escaped = true;
|
|
158
|
+
} else if (character === '"') {
|
|
159
|
+
current += character;
|
|
160
|
+
quoted = !quoted;
|
|
161
|
+
} else if (character === separator && !quoted) {
|
|
162
|
+
parts.push(current);
|
|
163
|
+
current = "";
|
|
164
|
+
} else {
|
|
165
|
+
current += character;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
parts.push(current);
|
|
170
|
+
return parts;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function unquote(value) {
|
|
174
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
175
|
+
return value.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
176
|
+
}
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isPagePath(pathname) {
|
|
181
|
+
const finalSegment = pathname.split("/").pop();
|
|
182
|
+
return pathname.endsWith("/") || /\.html?$/.test(pathname) || !finalSegment.includes(".") || pathname.endsWith(".md");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function variantPaths(pathname, explicitMarkdown) {
|
|
186
|
+
if (explicitMarkdown) return {html: htmlPathFor(pathname), markdown: pathname};
|
|
187
|
+
return {html: pathname, markdown: markdownPathFor(pathname)};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function markdownPathFor(pathname) {
|
|
191
|
+
if (pathname === "/") return "/index.md";
|
|
192
|
+
if (pathname.endsWith("/")) return `${pathname.slice(0, -1)}.md`;
|
|
193
|
+
if (/\.html?$/.test(pathname)) return pathname.replace(/\.html?$/, ".md");
|
|
194
|
+
return `${pathname}.md`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function htmlPathFor(pathname) {
|
|
198
|
+
if (pathname === "/index.md") return "/";
|
|
199
|
+
return `${pathname.slice(0, -3)}/`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function decorate(response, selected, paths, negotiated) {
|
|
203
|
+
const headers = new Headers(response.headers);
|
|
204
|
+
if (negotiated) mergeVary(headers, "Accept");
|
|
205
|
+
|
|
206
|
+
if (response.status < 200 || response.status >= 300) {
|
|
207
|
+
return new Response(response.body, {
|
|
208
|
+
status: response.status,
|
|
209
|
+
statusText: response.statusText,
|
|
210
|
+
headers
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const representation = selected === "markdown" ? MARKDOWN : HTML;
|
|
215
|
+
headers.set("Content-Type", representation.contentType);
|
|
216
|
+
appendLink(headers, alternateLink(selected, paths));
|
|
217
|
+
|
|
218
|
+
return new Response(response.body, {
|
|
219
|
+
status: response.status,
|
|
220
|
+
statusText: response.statusText,
|
|
221
|
+
headers
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function alternateLink(selected, paths) {
|
|
226
|
+
const alternate = selected === "markdown" ? "html" : "markdown";
|
|
227
|
+
return `<${paths[alternate]}>; rel="alternate"; type="text/${alternate}"`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function appendLink(headers, alternate) {
|
|
231
|
+
const current = headers.get("Link");
|
|
232
|
+
headers.set("Link", mergeLink(current, alternate));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function mergeLink(current, alternate) {
|
|
236
|
+
if (!current || current.includes(alternate)) return current || alternate;
|
|
237
|
+
return `${current}, ${alternate}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function mergeVary(headers, field) {
|
|
241
|
+
const fields = (headers.get("Vary") || "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
242
|
+
if (!fields.some((value) => value.toLowerCase() === field.toLowerCase())) fields.push(field);
|
|
243
|
+
headers.set("Vary", fields.join(", "));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function notAcceptable() {
|
|
247
|
+
return new Response("Not Acceptable\n", {
|
|
248
|
+
status: 406,
|
|
249
|
+
headers: {
|
|
250
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
251
|
+
Vary: "Accept"
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
type EdgeContext = {
|
|
2
|
+
next(): Promise<Response>;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
type Representation = {
|
|
6
|
+
name: "html" | "markdown";
|
|
7
|
+
contentType: string;
|
|
8
|
+
type: string;
|
|
9
|
+
subtype: string;
|
|
10
|
+
parameters: Record<string, string>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type MediaRange = {
|
|
14
|
+
type: string;
|
|
15
|
+
subtype: string;
|
|
16
|
+
quality: number;
|
|
17
|
+
parameters: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Variant = "html" | "markdown";
|
|
21
|
+
|
|
22
|
+
type Preference = {
|
|
23
|
+
selected: Variant | null;
|
|
24
|
+
htmlAcceptable: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const HTML: Representation = {
|
|
28
|
+
name: "html",
|
|
29
|
+
contentType: "text/html; charset=utf-8",
|
|
30
|
+
type: "text",
|
|
31
|
+
subtype: "html",
|
|
32
|
+
parameters: {charset: "utf-8"}
|
|
33
|
+
};
|
|
34
|
+
const MARKDOWN: Representation = {
|
|
35
|
+
name: "markdown",
|
|
36
|
+
contentType: "text/markdown; charset=utf-8",
|
|
37
|
+
type: "text",
|
|
38
|
+
subtype: "markdown",
|
|
39
|
+
parameters: {charset: "utf-8"}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default async (request: Request, context: EdgeContext): Promise<Response> => {
|
|
43
|
+
const url = new URL(request.url);
|
|
44
|
+
const explicitMarkdown = url.pathname.endsWith(".md");
|
|
45
|
+
|
|
46
|
+
if (!isPagePath(url.pathname)) return context.next();
|
|
47
|
+
|
|
48
|
+
const preference: Preference = explicitMarkdown
|
|
49
|
+
? {selected: "markdown", htmlAcceptable: false}
|
|
50
|
+
: negotiate(request.headers.get("Accept"));
|
|
51
|
+
if (!preference.selected) return notAcceptable();
|
|
52
|
+
|
|
53
|
+
const paths = variantPaths(url.pathname, explicitMarkdown);
|
|
54
|
+
const {response, served} = await fetchVariant(request, context, url, paths, preference, explicitMarkdown);
|
|
55
|
+
|
|
56
|
+
return decorate(response, served, paths, !explicitMarkdown);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// A negotiated Markdown variant is not guaranteed to exist: the plugin exports
|
|
60
|
+
// posts by default, so most page URLs have no .md sibling until the site opts
|
|
61
|
+
// pages or collections in. Fall back to HTML when the client accepts it rather
|
|
62
|
+
// than turning a page the host could serve into a 404.
|
|
63
|
+
async function fetchVariant(
|
|
64
|
+
request: Request,
|
|
65
|
+
context: EdgeContext,
|
|
66
|
+
url: URL,
|
|
67
|
+
paths: Record<Variant, string>,
|
|
68
|
+
preference: Preference,
|
|
69
|
+
explicitMarkdown: boolean
|
|
70
|
+
): Promise<{response: Response; served: Variant}> {
|
|
71
|
+
const selected = preference.selected as Variant;
|
|
72
|
+
if (selected === "html" || explicitMarkdown) return {response: await context.next(), served: selected};
|
|
73
|
+
|
|
74
|
+
const response = await fetch(new Request(new URL(paths.markdown, url), request));
|
|
75
|
+
if (response.status !== 404 || !preference.htmlAcceptable) return {response, served: "markdown"};
|
|
76
|
+
|
|
77
|
+
return {response: await context.next(), served: "html"};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function negotiate(accept: string | null): Preference {
|
|
81
|
+
if (!accept || !accept.trim()) return {selected: "html", htmlAcceptable: true};
|
|
82
|
+
|
|
83
|
+
const ranges = parseAccept(accept);
|
|
84
|
+
const html = qualityFor(HTML, ranges);
|
|
85
|
+
const markdown = qualityFor(MARKDOWN, ranges);
|
|
86
|
+
const htmlAcceptable = html.quality > 0;
|
|
87
|
+
|
|
88
|
+
if (html.quality <= 0 && markdown.quality <= 0) return {selected: null, htmlAcceptable};
|
|
89
|
+
if (markdown.quality > html.quality) return {selected: "markdown", htmlAcceptable};
|
|
90
|
+
if (html.quality > markdown.quality) return {selected: "html", htmlAcceptable};
|
|
91
|
+
|
|
92
|
+
const explicitMarkdown = ranges.some((range) =>
|
|
93
|
+
range.type === "text" &&
|
|
94
|
+
range.subtype === "markdown" &&
|
|
95
|
+
range.quality > 0 &&
|
|
96
|
+
specificity(range, MARKDOWN) !== null
|
|
97
|
+
);
|
|
98
|
+
return {selected: explicitMarkdown ? "markdown" : "html", htmlAcceptable};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function qualityFor(representation: Representation, ranges: MediaRange[]) {
|
|
102
|
+
let bestSpecificity = -1;
|
|
103
|
+
let quality = 0;
|
|
104
|
+
|
|
105
|
+
for (const range of ranges) {
|
|
106
|
+
const rangeSpecificity = specificity(range, representation);
|
|
107
|
+
if (rangeSpecificity === null || rangeSpecificity < bestSpecificity) continue;
|
|
108
|
+
|
|
109
|
+
if (rangeSpecificity > bestSpecificity) {
|
|
110
|
+
bestSpecificity = rangeSpecificity;
|
|
111
|
+
quality = range.quality;
|
|
112
|
+
} else {
|
|
113
|
+
quality = Math.max(quality, range.quality);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {quality, specificity: bestSpecificity};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function specificity(range: MediaRange, representation: Representation): number | null {
|
|
121
|
+
if (range.type === "*" && range.subtype !== "*") return null;
|
|
122
|
+
if (range.type !== "*" && range.type !== representation.type) return null;
|
|
123
|
+
if (range.subtype !== "*" && range.subtype !== representation.subtype) return null;
|
|
124
|
+
|
|
125
|
+
for (const [name, value] of Object.entries(range.parameters)) {
|
|
126
|
+
if (representation.parameters[name] !== value) return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const mediaSpecificity = range.type === "*" ? 0 : range.subtype === "*" ? 1 : 2;
|
|
130
|
+
return (mediaSpecificity * 100) + Object.keys(range.parameters).length;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseAccept(accept: string): MediaRange[] {
|
|
134
|
+
return splitQuoted(accept, ",").map(parseRange).filter((range): range is MediaRange => range !== null);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseRange(source: string): MediaRange | null {
|
|
138
|
+
const parts = splitQuoted(source, ";");
|
|
139
|
+
const media = parts.shift()!.trim().toLowerCase();
|
|
140
|
+
const match = media.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
141
|
+
if (!match) return null;
|
|
142
|
+
|
|
143
|
+
const range: MediaRange = {type: match[1], subtype: match[2], quality: 1, parameters: {}};
|
|
144
|
+
for (const sourceParameter of parts) {
|
|
145
|
+
const separator = sourceParameter.indexOf("=");
|
|
146
|
+
if (separator < 1) {
|
|
147
|
+
if (sourceParameter.trim().toLowerCase() === "q") range.quality = 0;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const name = sourceParameter.slice(0, separator).trim().toLowerCase();
|
|
152
|
+
const rawValue = sourceParameter.slice(separator + 1).trim();
|
|
153
|
+
if (name === "q") {
|
|
154
|
+
range.quality = parseQuality(rawValue.toLowerCase());
|
|
155
|
+
} else {
|
|
156
|
+
range.parameters[name] = unquote(rawValue).toLowerCase();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return range;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseQuality(value: string): number {
|
|
164
|
+
return /^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/.test(value) ? Number(value) : 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function splitQuoted(source: string, separator: string): string[] {
|
|
168
|
+
const parts: string[] = [];
|
|
169
|
+
let current = "";
|
|
170
|
+
let quoted = false;
|
|
171
|
+
let escaped = false;
|
|
172
|
+
|
|
173
|
+
for (const character of source) {
|
|
174
|
+
if (escaped) {
|
|
175
|
+
current += character;
|
|
176
|
+
escaped = false;
|
|
177
|
+
} else if (character === "\\" && quoted) {
|
|
178
|
+
current += character;
|
|
179
|
+
escaped = true;
|
|
180
|
+
} else if (character === '"') {
|
|
181
|
+
current += character;
|
|
182
|
+
quoted = !quoted;
|
|
183
|
+
} else if (character === separator && !quoted) {
|
|
184
|
+
parts.push(current);
|
|
185
|
+
current = "";
|
|
186
|
+
} else {
|
|
187
|
+
current += character;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
parts.push(current);
|
|
192
|
+
return parts;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function unquote(value: string): string {
|
|
196
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
197
|
+
return value.slice(1, -1).replace(/\\(.)/g, "$1");
|
|
198
|
+
}
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isPagePath(pathname: string): boolean {
|
|
203
|
+
const finalSegment = pathname.split("/").pop()!;
|
|
204
|
+
return pathname.endsWith("/") || /\.html?$/.test(pathname) || !finalSegment.includes(".") || pathname.endsWith(".md");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function variantPaths(pathname: string, explicitMarkdown: boolean): Record<Variant, string> {
|
|
208
|
+
if (explicitMarkdown) return {html: htmlPathFor(pathname), markdown: pathname};
|
|
209
|
+
return {html: pathname, markdown: markdownPathFor(pathname)};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function markdownPathFor(pathname: string): string {
|
|
213
|
+
if (pathname === "/") return "/index.md";
|
|
214
|
+
if (pathname.endsWith("/")) return `${pathname.slice(0, -1)}.md`;
|
|
215
|
+
if (/\.html?$/.test(pathname)) return pathname.replace(/\.html?$/, ".md");
|
|
216
|
+
return `${pathname}.md`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function htmlPathFor(pathname: string): string {
|
|
220
|
+
if (pathname === "/index.md") return "/";
|
|
221
|
+
return `${pathname.slice(0, -3)}/`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function decorate(
|
|
225
|
+
response: Response,
|
|
226
|
+
selected: Variant,
|
|
227
|
+
paths: Record<Variant, string>,
|
|
228
|
+
negotiated: boolean
|
|
229
|
+
): Response {
|
|
230
|
+
const headers = new Headers(response.headers);
|
|
231
|
+
if (negotiated) mergeVary(headers, "Accept");
|
|
232
|
+
|
|
233
|
+
if (response.status < 200 || response.status >= 300) {
|
|
234
|
+
return new Response(response.body, {
|
|
235
|
+
status: response.status,
|
|
236
|
+
statusText: response.statusText,
|
|
237
|
+
headers
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const representation = selected === "markdown" ? MARKDOWN : HTML;
|
|
242
|
+
headers.set("Content-Type", representation.contentType);
|
|
243
|
+
appendLink(headers, alternateLink(selected, paths));
|
|
244
|
+
|
|
245
|
+
return new Response(response.body, {
|
|
246
|
+
status: response.status,
|
|
247
|
+
statusText: response.statusText,
|
|
248
|
+
headers
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function alternateLink(selected: Variant, paths: Record<Variant, string>): string {
|
|
253
|
+
const alternate = selected === "markdown" ? "html" : "markdown";
|
|
254
|
+
return `<${paths[alternate]}>; rel="alternate"; type="text/${alternate}"`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function appendLink(headers: Headers, alternate: string): void {
|
|
258
|
+
const current = headers.get("Link");
|
|
259
|
+
headers.set("Link", mergeLink(current, alternate));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function mergeLink(current: string | null, alternate: string): string {
|
|
263
|
+
if (!current || current.includes(alternate)) return current || alternate;
|
|
264
|
+
return `${current}, ${alternate}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function mergeVary(headers: Headers, field: string): void {
|
|
268
|
+
const fields = (headers.get("Vary") || "").split(",").map((value) => value.trim()).filter(Boolean);
|
|
269
|
+
if (!fields.some((value) => value.toLowerCase() === field.toLowerCase())) fields.push(field);
|
|
270
|
+
headers.set("Vary", fields.join(", "));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function notAcceptable(): Response {
|
|
274
|
+
return new Response("Not Acceptable\n", {
|
|
275
|
+
status: 406,
|
|
276
|
+
headers: {
|
|
277
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
278
|
+
Vary: "Accept"
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|