@mdream/js
Advanced tools
+22
-10
@@ -0,1 +1,2 @@ | ||
| type ContentNegotiationResult = 'markdown' | 'html' | 'not-acceptable'; | ||
| interface AcceptEntry { | ||
@@ -12,14 +13,25 @@ type: string; | ||
| /** | ||
| * Determine if a client prefers markdown over HTML using proper content negotiation. | ||
| * Perform RFC 7231 content negotiation for HTML vs Markdown. | ||
| * | ||
| * Uses Accept header quality weights and position ordering: | ||
| * - If text/markdown or text/plain has higher quality than text/html -> markdown | ||
| * - If same quality, earlier position in Accept header wins | ||
| * - Bare wildcard does NOT trigger markdown (prevents breaking OG crawlers) | ||
| * - sec-fetch-dest: document always returns false (browser navigation) | ||
| * | ||
| * @param acceptHeader - The HTTP Accept header value | ||
| * @param secFetchDest - The Sec-Fetch-Dest header value | ||
| * Resolution rules: | ||
| * - `Sec-Fetch-Dest: document` always returns `'html'` (browser navigation). | ||
| * - Missing or empty Accept header returns `'html'` (server picks default). | ||
| * - q=0 entries are treated as explicit rejections and ignored for matching | ||
| * (but still count towards "something was listed"). | ||
| * - `text/markdown` and `text/plain` are the markdown-capable types. | ||
| * - `text/html` and `application/xhtml+xml` are the html-capable types. | ||
| * - `*_/_*` and `text/*` are wildcards; they satisfy 406 but never on their | ||
| * own tip negotiation towards markdown (preserves OG crawler behavior). | ||
| * - If nothing in the Accept header can be served (no explicit match, no | ||
| * wildcard), returns `'not-acceptable'` so the caller can send 406. | ||
| * - Otherwise, compares best markdown entry vs best html-or-wildcard entry | ||
| * by q, then by position. | ||
| */ | ||
| declare function negotiateContent(acceptHeader?: string, secFetchDest?: string): ContentNegotiationResult; | ||
| /** | ||
| * Determine if a client prefers markdown over HTML. Convenience wrapper over | ||
| * {@link negotiateContent}; treats `'not-acceptable'` the same as `'html'` | ||
| * (callers that want 406 semantics should use `negotiateContent` directly). | ||
| */ | ||
| declare function shouldServeMarkdown(acceptHeader?: string, secFetchDest?: string): boolean; | ||
| export { parseAcceptHeader, shouldServeMarkdown }; | ||
| export { ContentNegotiationResult, negotiateContent, parseAcceptHeader, shouldServeMarkdown }; |
+65
-18
@@ -31,14 +31,21 @@ function parseAcceptHeader(accept) { | ||
| } | ||
| function shouldServeMarkdown(acceptHeader, secFetchDest) { | ||
| if (secFetchDest === "document") return false; | ||
| function negotiateContent(acceptHeader, secFetchDest) { | ||
| if (secFetchDest === "document") return "html"; | ||
| const accept = acceptHeader || ""; | ||
| if (!accept) return false; | ||
| const parts = accept.split(","); | ||
| if (!accept) return "html"; | ||
| let bestMdQ = -1; | ||
| let bestMdPos = -1; | ||
| let htmlQ = -1; | ||
| let htmlPos = -1; | ||
| let bestHtmlQ = -1; | ||
| let bestHtmlPos = -1; | ||
| let bestWildcardQ = -1; | ||
| let bestWildcardPos = -1; | ||
| let sawAnyEntry = false; | ||
| let sawAcceptable = false; | ||
| let rejectedMd = false; | ||
| let rejectedHtml = false; | ||
| const parts = accept.split(","); | ||
| for (let i = 0; i < parts.length; i++) { | ||
| const part = parts[i].trim(); | ||
| if (!part) continue; | ||
| sawAnyEntry = true; | ||
| const semicolonIdx = part.indexOf(";"); | ||
@@ -51,3 +58,10 @@ let type; | ||
| const paramStr = part.slice(semicolonIdx + 1); | ||
| const qIdx = paramStr.indexOf("q="); | ||
| let qIdx = -1; | ||
| for (let j = 0; j < paramStr.length - 1; j++) { | ||
| const c = paramStr.charCodeAt(j); | ||
| if ((c === 113 || c === 81) && paramStr.charCodeAt(j + 1) === 61) { | ||
| qIdx = j; | ||
| break; | ||
| } | ||
| } | ||
| if (qIdx !== -1) { | ||
@@ -60,18 +74,51 @@ const qStart = qIdx + 2; | ||
| } | ||
| if (type === "text/markdown" || type === "text/plain") { | ||
| if (q > bestMdQ || q === bestMdQ && (bestMdPos === -1 || i < bestMdPos)) { | ||
| const normalized = type.toLowerCase(); | ||
| if (normalized === "text/markdown" || normalized === "text/plain") { | ||
| if (q === 0) { | ||
| rejectedMd = true; | ||
| continue; | ||
| } | ||
| sawAcceptable = true; | ||
| if (q > bestMdQ || q === bestMdQ && bestMdPos === -1) { | ||
| bestMdQ = q; | ||
| bestMdPos = i; | ||
| } | ||
| } else if (type === "text/html") { | ||
| htmlQ = q; | ||
| htmlPos = i; | ||
| } else if (normalized === "text/html" || normalized === "application/xhtml+xml") { | ||
| if (q === 0) { | ||
| rejectedHtml = true; | ||
| continue; | ||
| } | ||
| sawAcceptable = true; | ||
| if (q > bestHtmlQ || q === bestHtmlQ && bestHtmlPos === -1) { | ||
| bestHtmlQ = q; | ||
| bestHtmlPos = i; | ||
| } | ||
| } else if (normalized === "*/*" || normalized === "text/*") { | ||
| if (q === 0) continue; | ||
| sawAcceptable = true; | ||
| if (q > bestWildcardQ || q === bestWildcardQ && bestWildcardPos === -1) { | ||
| bestWildcardQ = q; | ||
| bestWildcardPos = i; | ||
| } | ||
| } | ||
| } | ||
| if (bestMdPos === -1) return false; | ||
| if (htmlPos === -1) return true; | ||
| if (bestMdQ > htmlQ) return true; | ||
| if (bestMdQ === htmlQ && bestMdPos < htmlPos) return true; | ||
| return false; | ||
| if (sawAnyEntry && !sawAcceptable) return "not-acceptable"; | ||
| if (bestMdPos === -1 && !rejectedMd && bestWildcardPos !== -1) { | ||
| bestMdQ = bestWildcardQ; | ||
| bestMdPos = bestWildcardPos; | ||
| } | ||
| if (bestHtmlPos === -1 && !rejectedHtml && bestWildcardPos !== -1) { | ||
| bestHtmlQ = bestWildcardQ; | ||
| bestHtmlPos = bestWildcardPos; | ||
| } | ||
| if (bestMdPos === -1 && bestHtmlPos === -1) return "not-acceptable"; | ||
| if (bestMdPos === -1) return "html"; | ||
| if (bestHtmlPos === -1) return "markdown"; | ||
| if (bestMdQ > bestHtmlQ) return "markdown"; | ||
| if (bestMdQ === bestHtmlQ && bestMdPos < bestHtmlPos) return "markdown"; | ||
| return "html"; | ||
| } | ||
| export { parseAcceptHeader, shouldServeMarkdown }; | ||
| function shouldServeMarkdown(acceptHeader, secFetchDest) { | ||
| return negotiateContent(acceptHeader, secFetchDest) === "markdown"; | ||
| } | ||
| export { negotiateContent, parseAcceptHeader, shouldServeMarkdown }; |
+1
-1
| { | ||
| "name": "@mdream/js", | ||
| "type": "module", | ||
| "version": "1.0.7", | ||
| "version": "1.1.0", | ||
| "description": "JavaScript HTML-to-Markdown engine for mdream. Escape hatch for hooks and edge runtimes.", | ||
@@ -6,0 +6,0 @@ "author": { |
159218
1.49%3515
1.36%