| 1 | (async () => { |
| 2 | const readme = document.querySelector("#readme .markdown"); |
| 3 | if (!readme || !window.markdownit || !window.DOMPurify) return; |
| 4 | |
| 5 | const response = await fetch("/snowbound/plain/main/readme.md"); |
| 6 | if (!response.ok) return; |
| 7 | |
| 8 | const markdown = await response.text(); |
| 9 | const rendered = window.markdownit({ html: true }).render(markdown); |
| 10 | const fragment = window.DOMPurify.sanitize(rendered, { |
| 11 | USE_PROFILES: { html: true }, |
| 12 | RETURN_DOM_FRAGMENT: true, |
| 13 | }); |
| 14 | |
| 15 | const mediaBase = new URL("/snowbound/plain/main/", location.origin); |
| 16 | const linkBase = new URL("/snowbound/tree/main/", location.origin); |
| 17 | const rebase = (value, base) => |
| 18 | value && !value.startsWith("/") && !value.startsWith("#") && !/^[a-z][a-z\d+.-]*:/i.test(value) |
| 19 | ? new URL(value, base).href |
| 20 | : value; |
| 21 | |
| 22 | for (const image of fragment.querySelectorAll("img[src]")) { |
| 23 | image.src = rebase(image.getAttribute("src"), mediaBase); |
| 24 | for (const dimension of ["width", "height"]) { |
| 25 | const value = image.getAttribute(dimension); |
| 26 | if (value && /^\d+$/.test(value)) image.style[dimension] = `${value}px`; |
| 27 | } |
| 28 | } |
| 29 | for (const pictureSource of fragment.querySelectorAll("source[srcset]")) { |
| 30 | pictureSource.srcset = pictureSource.srcset.split(",").map(candidate => { |
| 31 | const [, path, descriptor] = candidate.trim().match(/^(\S+)(.*)$/) || []; |
| 32 | return path ? rebase(path, mediaBase) + descriptor : candidate; |
| 33 | }).join(", "); |
| 34 | } |
| 35 | for (const link of fragment.querySelectorAll("a[href]")) { |
| 36 | link.href = rebase(link.getAttribute("href"), linkBase); |
| 37 | } |
| 38 | readme.replaceChildren(fragment); |
| 39 | })().catch(console.error); |