From 93a719a8098d02a876904ecdfee8bc044021ef05 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 27 Jan 2026 12:01:40 +0100 Subject: [PATCH 1/8] Add GitHub style markdown alerts Fixes #296 --- static/readme.js | 39 +++++++++++++++++++++++++++++++++++++++ static/style/readme.css | 13 +++++++++++++ 2 files changed, 52 insertions(+) diff --git a/static/readme.js b/static/readme.js index ce114f88c..89751a760 100644 --- a/static/readme.js +++ b/static/readme.js @@ -1,6 +1,45 @@ import DOMPurify from 'https://cdn.jsdelivr.net/npm/dompurify/dist/purify.es.mjs' import { marked } from 'https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js' +const markdownAlertExtension = { + name: 'markdownAlert', + level: 'block', + start(src) { + const match = src.match(/^> ?\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/m) + return match ? match.index : undefined + }, + tokenizer(src) { + const rule = /^> ?\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][^\n]*(?:\n> ?.*)*/ + const match = rule.exec(src) + if (!match) return + + const raw = match[0] + const alertType = match[1].toLowerCase() + const lines = raw.split('\n').map(line => line.replace(/^> ?/, '')) + lines.shift() + const text = lines.join('\n').replace(/^\n+/, '') + const tokens = this.lexer.blockTokens(text, []) + + return { + type: 'markdownAlert', + raw, + alertType, + tokens, + } + }, + renderer(token) { + const title = token.alertType.charAt(0).toUpperCase() + token.alertType.slice(1) + const body = this.parser.parse(token.tokens) + return `\ +
\ +

${title}

\ +

${body}

\ +
` + }, +} + +marked.use({ extensions: [markdownAlertExtension] }) + const target = document.getElementById('md') const source = target.dataset.readmeUrl diff --git a/static/style/readme.css b/static/style/readme.css index c0abf84fb..8188732a5 100644 --- a/static/style/readme.css +++ b/static/style/readme.css @@ -74,4 +74,17 @@ padding: 6px 13px; } } + + .markdown-alert { + border-left: 4px solid var(--foreground-2); + padding-left: 1em; + + .markdown-alert-title { + font-weight: 700; + + &:after { + content: ':'; + } + } + } } From 4bbec49162f30dbf3ed3d15d2f3ddb007cef7019 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 27 Jan 2026 12:59:47 +0100 Subject: [PATCH 2/8] Add heading anchors --- static/readme.js | 64 ++++++++++++++++++++++++++++++++++++++++- static/style/readme.css | 23 +++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/static/readme.js b/static/readme.js index 89751a760..6df587499 100644 --- a/static/readme.js +++ b/static/readme.js @@ -38,7 +38,22 @@ const markdownAlertExtension = { }, } -marked.use({ extensions: [markdownAlertExtension] }) +let headingSlugCounts = new Map() + +const readmeHeadingRenderer = { + heading({ text, depth: level } = {}) { + if (level > 4) { + return `${text}` + } + + const slug = unique_heading_slug(text) + const id = `readme-${slug}` + return `\ +${text}` + }, +} + +marked.use({ extensions: [markdownAlertExtension], renderer: readmeHeadingRenderer }) const target = document.getElementById('md') const source = target.dataset.readmeUrl @@ -51,6 +66,7 @@ const ttl = 60 * 60 // 1 hour in seconds if (cached && (now - cached.time) < ttl) { target.innerHTML = cached.html + scroll_readme_anchor() } else { fetch(source) @@ -60,10 +76,12 @@ else { }) .then((md) => { if (DOMPurify.isSupported && is_markdown(source)) { + headingSlugCounts = new Map() const html = marked.parse(md) const html_ = post_process_html(html, source) const safe_content = DOMPurify.sanitize(html_) target.innerHTML = safe_content + scroll_readme_anchor() } else { const escaped = md @@ -93,12 +111,56 @@ function is_markdown(url) { return /(readme|\.md|\.mkd|\.mdown|\.markdown|\.txt)$/i.test(url) } +function scroll_readme_anchor() { + const url = new URL(window.location.href) + if (!url.searchParams.has('readme')) { + return + } + + const slug = url.hash.replace(/^#/, '') + if (!slug) { + return + } + + const anchor = document.getElementById(`readme-${slug}`) + if (anchor) { + anchor.scrollIntoView({ block: 'start' }) + } +} + +function unique_heading_slug(text) { + // Avoid duplicate ids/anchors when headings share the same text. + const base = slugify_heading(text) + const count = headingSlugCounts.get(base) ?? 0 + headingSlugCounts.set(base, count + 1) + if (count === 0) { + return base + } + + return `${base}-${count}` +} + +function slugify_heading(raw) { + return String(raw ?? '') + .toLowerCase() + .trim() + .replace(/<[^>]*>/g, '') + .replace(/[^a-z0-9\s-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-') + .replace(/^-+|-+$/g, '') +} + function post_process_html(html, base_url) { const parser = new DOMParser() const doc = parser.parseFromString(html, 'text/html') const base = new URL(base_url) doc.querySelectorAll('a[href], img[src], video[src]').forEach((el) => { + if (el.matches('a.markdown-anchor')) { + return + } + const attr = el.hasAttribute('href') ? 'href' : 'src' const val = el.getAttribute(attr) if (val && !val.match(/^([a-z]+:|#|\/)/i)) { diff --git a/static/style/readme.css b/static/style/readme.css index 8188732a5..445887190 100644 --- a/static/style/readme.css +++ b/static/style/readme.css @@ -87,4 +87,27 @@ } } } + + h1, h2, h3, h4 { + position: relative; + + &:hover, &:focus-within { + .markdown-anchor { + opacity: 1; + } + } + } + + .markdown-anchor { + opacity: 0; + position: absolute; + left: -1em; + bottom: 0.1em; + color: var(--foreground-3); + width: 1em; + text-decoration: none; + } + .markdown-anchor:after { + content: "⎆"; + } } From 4d8568634a34ff3ce3323c79f4001638f150f619 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 27 Jan 2026 13:18:40 +0100 Subject: [PATCH 3/8] Render readme blocking if cached --- packages/package.njk | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/package.njk b/packages/package.njk index d3d0f882e..19712a4c8 100644 --- a/packages/package.njk +++ b/packages/package.njk @@ -156,6 +156,29 @@ isPackagePage: true {% if pkg.readme and not pkg.removed %}
+ {% endif %} From d5411d0c3fe6171d6904992061ad8e298fecdc8b Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 27 Jan 2026 14:25:33 +0100 Subject: [PATCH 4/8] Colorize .markdown-alert --- static/style/readme.css | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/static/style/readme.css b/static/style/readme.css index 445887190..47eef0601 100644 --- a/static/style/readme.css +++ b/static/style/readme.css @@ -79,6 +79,24 @@ border-left: 4px solid var(--foreground-2); padding-left: 1em; + &.markdown-alert-note, + &.markdown-alert-tip, + &.markdown-alert-important, + &.markdown-alert-warning, + &.markdown-alert-caution { + border-left-color: hsl(29.62deg 66.08% 57.86%); + } + + :root[data-theme='dark'] & { + &.markdown-alert-note, + &.markdown-alert-tip, + &.markdown-alert-important, + &.markdown-alert-warning, + &.markdown-alert-caution { + border-left-color: hsl(32.77deg 70.44% 55.43%); + } + } + .markdown-alert-title { font-weight: 700; From 82058b5367446013c69de0819d4d283262e98efc Mon Sep 17 00:00:00 2001 From: herr kaste Date: Tue, 27 Jan 2026 12:23:39 +0100 Subject: [PATCH 5/8] Underline the links in the README --- static/style/readme.css | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/static/style/readme.css b/static/style/readme.css index 47eef0601..70749ce43 100644 --- a/static/style/readme.css +++ b/static/style/readme.css @@ -56,7 +56,7 @@ border-radius: 6px; line-height: 1.4rem; padding: 0px 4px; - box-shadow: inset 0px -1px 0px var(--foreground-4); + box-shadow: inset 0px -1px 0px var(--background-1); background: var(--highlight); } @@ -75,6 +75,18 @@ } } + a { + color: hsl(31.78deg 88.74% 37.93%); + text-decoration-color: hsl(36.11deg 97.24% 42.55%); + text-decoration: underline; + text-underline-offset: 2px; + + :root[data-theme='dark'] & { + color: hsl(32.41deg 73.83% 59.86%); + text-decoration-color: #d68a33; + } + } + .markdown-alert { border-left: 4px solid var(--foreground-2); padding-left: 1em; From 1cce77fe5c36fa18f279c912977a0f9b757429f0 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Thu, 29 Jan 2026 22:56:06 +0100 Subject: [PATCH 6/8] Address feedback --- static/readme.js | 12 ++++++++--- static/style/readme.css | 45 ++++++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/static/readme.js b/static/readme.js index 6df587499..06e9cdfca 100644 --- a/static/readme.js +++ b/static/readme.js @@ -49,7 +49,11 @@ const readmeHeadingRenderer = { const slug = unique_heading_slug(text) const id = `readme-${slug}` return `\ -${text}` +${text}\ +\ +\ +\ +` }, } @@ -59,7 +63,7 @@ const target = document.getElementById('md') const source = target.dataset.readmeUrl const cacheKey = 'md:' + source -const cached = JSON.parse(sessionStorage.getItem(cacheKey) || 'null') +const cached = false && JSON.parse(sessionStorage.getItem(cacheKey) || 'null') const now = Math.floor(Date.now() / 1000) const ttl = 60 * 60 // 1 hour in seconds @@ -79,7 +83,9 @@ else { headingSlugCounts = new Map() const html = marked.parse(md) const html_ = post_process_html(html, source) - const safe_content = DOMPurify.sanitize(html_) + const safe_content = DOMPurify.sanitize(html_, { + USE_PROFILES: { html: true, svg: true, svgFilters: true }, + }) target.innerHTML = safe_content scroll_readme_anchor() } diff --git a/static/style/readme.css b/static/style/readme.css index 70749ce43..2894852d9 100644 --- a/static/style/readme.css +++ b/static/style/readme.css @@ -76,14 +76,22 @@ } a { - color: hsl(31.78deg 88.74% 37.93%); - text-decoration-color: hsl(36.11deg 97.24% 42.55%); - text-decoration: underline; - text-underline-offset: 2px; + color: var(--foreground-1); + text-decoration-color: var(--foreground-4); + text-underline-offset: 4px; + transition: color .1s ease-in-out, outline .1s ease-in-out, text-decoration-color .1s ease-in-out; :root[data-theme='dark'] & { - color: hsl(32.41deg 73.83% 59.86%); - text-decoration-color: #d68a33; + text-decoration-color: var(--foreground-2); + + &:hover { + text-decoration-color: var(--orange-2); + } + } + + &:hover { + color: var(--orange-2); + text-decoration-color: var(--orange-2); } } @@ -96,16 +104,9 @@ &.markdown-alert-important, &.markdown-alert-warning, &.markdown-alert-caution { - border-left-color: hsl(29.62deg 66.08% 57.86%); - } + border-left-color: hsl(183.19deg 75.11% 44.29%); - :root[data-theme='dark'] & { - &.markdown-alert-note, - &.markdown-alert-tip, - &.markdown-alert-important, - &.markdown-alert-warning, - &.markdown-alert-caution { - border-left-color: hsl(32.77deg 70.44% 55.43%); + :root[data-theme='dark'] & { } } @@ -130,14 +131,16 @@ .markdown-anchor { opacity: 0; + display: flex; + align-items: center; + justify-content: center; position: absolute; - left: -1em; - bottom: 0.1em; - color: var(--foreground-3); + left: -1.3em; + bottom: 6px; + color: hsl(240deg 6.3% 43.09%); + color: var(--foreground-2); width: 1em; + height: 1em; text-decoration: none; } - .markdown-anchor:after { - content: "⎆"; - } } From f234f55851c73f2bc9c6d07306c8bd628c40a707 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Thu, 29 Jan 2026 23:16:14 +0100 Subject: [PATCH 7/8] Improve hover experience for the anchored headings --- static/style/readme.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/style/readme.css b/static/style/readme.css index 2894852d9..b63aeef45 100644 --- a/static/style/readme.css +++ b/static/style/readme.css @@ -121,6 +121,8 @@ h1, h2, h3, h4 { position: relative; + padding-left: 1.3em; + margin-left: -1.3em; &:hover, &:focus-within { .markdown-anchor { @@ -135,7 +137,7 @@ align-items: center; justify-content: center; position: absolute; - left: -1.3em; + left: 0; bottom: 6px; color: hsl(240deg 6.3% 43.09%); color: var(--foreground-2); From a836165465178b3a71e91ff3ad985a67c28e7cb1 Mon Sep 17 00:00:00 2001 From: herr kaste Date: Thu, 29 Jan 2026 23:33:49 +0100 Subject: [PATCH 8/8] Improve anchor position --- static/style/readme.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/style/readme.css b/static/style/readme.css index b63aeef45..ed20cf551 100644 --- a/static/style/readme.css +++ b/static/style/readme.css @@ -121,8 +121,8 @@ h1, h2, h3, h4 { position: relative; - padding-left: 1.3em; - margin-left: -1.3em; + padding-left: 2.4rem; + margin-left: -2.4rem; &:hover, &:focus-within { .markdown-anchor { @@ -138,11 +138,11 @@ justify-content: center; position: absolute; left: 0; - bottom: 6px; + bottom: 0.25em; color: hsl(240deg 6.3% 43.09%); color: var(--foreground-2); - width: 1em; - height: 1em; + width: 28px; + height: 28px; text-decoration: none; } }