Something went wrong. Try again.
Something went wrong. Try again.
2.4 kB · 77 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778import { parse } from 'node-html-parser';
// Fandom/MediaWiki cruft that adds no value in a reader UI.const REMOVE_SELECTORS = [ 'script', '.mw-editsection', '.mw-empty-elt', // ad rails / sidebars occasionally present in parsed output '.wds-global-footer', '.page-side-tools', '[data-ad]', '.ad-slot', // Fandom-injected FAQ/engagement widgets (trfc161 etc.) '[class^="trfc"]', // caption info icons reference Fandom's SVG sprite sheet, which we don't serve '.info-icon'];
/** * Sanitize and rewrite a MediaWiki `parse` HTML fragment for our reader UI. * * - Internal `/wiki/X` links are rewritten to `/<slug>/wiki/X`; category links * `/wiki/Category:X` become `/<slug>/category/X`. Query/hash preserved. * - External (http/https) links get `target="_blank"` + safe `rel`. * - `<script>`, edit sections and known empty/cruft elements are removed. * Inline `<style>` is preserved (infoboxes depend on it). * - Images become lazy/async-decoding; Fandom `data-src` is promoted to `src`; * `srcset` is dropped only when it points at `data:` placeholders. */export function transformArticleHtml(html: string, slug: string): string { const root = parse(html, { comment: false, voidTag: { closingSlash: true } });
for (const selector of REMOVE_SELECTORS) { for (const el of root.querySelectorAll(selector)) { el.remove(); } }
for (const a of root.querySelectorAll('a')) { const href = a.getAttribute('href'); if (!href) continue;
if (href.startsWith('/wiki/')) { const rest = href.slice('/wiki/'.length); if (rest.startsWith('Category:')) { a.setAttribute('href', `/${slug}/category/${rest.slice('Category:'.length)}`); } else { a.setAttribute('href', `/${slug}/wiki/${rest}`); } } else if (/^https?:\/\//i.test(href)) { a.setAttribute('target', '_blank'); a.setAttribute('rel', 'noopener noreferrer'); } }
for (const img of root.querySelectorAll('img')) { img.setAttribute('loading', 'lazy'); img.setAttribute('decoding', 'async'); // Fandom's image CDN 404s hotlinked requests that carry a foreign Referer img.setAttribute('referrerpolicy', 'no-referrer');
const dataSrc = img.getAttribute('data-src'); if (dataSrc) { img.setAttribute('src', dataSrc); }
const srcset = img.getAttribute('srcset'); if (srcset && srcset.includes('data:')) { img.removeAttribute('srcset'); } }
return root.toString();}