forked from akai/readest
a1279a65ce
Builds the URL-clipping path of the "Send to Readest" feature: paste a
link, the renderer ingests the rendered page, and a self-contained EPUB
lands in the library. No server proxy, no external CDN refs left in the
EPUB once it's saved.
Architecture
- New Rust `clip_url` command spawns a hidden Tauri WebviewWindow at the
target URL with a real Chrome UA + WebKit fingerprint mask, so TLS-
fingerprint and JS-challenge walls (Cloudflare, Medium, X, WeChat MP)
resolve naturally instead of bouncing the server proxy.
- Capture transport is URL-payload navigation to a one-shot
127.0.0.1:RANDOM_PORT/clip/{token}?d={url-safe-base64} listener.
Top-level navigation isn't governed by CSP connect-src / form-action /
WebKit Private Network Access — the four earlier transports
(fetch, <form>, custom URI scheme, window.name) were each blocked by
one of those.
- Page-to-EPUB bundler (`assetBundler`) walks <img>/<picture> with
src → data-src → data-original → data-srcset → srcset fallback so lazy-
loading sites don't ship a 60px LQIP; fetches assets in parallel with a
per-asset timeout + per-asset/total caps; failed images degrade to alt-
text placeholders. A per-site rules table (seeded with WeChat MP) + a
selector fallback catches articles Readability misextracts. Builder
prepends the article <h1> + byline so the EPUB has a proper opening.
- Nested EPUB TOC built from h1–h6.
UI surfaces
- "From Web URL" entry in the library Import menu, gated to Tauri; web
build hides the URL field and points at the browser extension.
- `ImportFromUrlDialog` with auto-height (overrides Dialog's `sm:h-[65%]`
default) and a dim placeholder for the URL field.
- Clip webview window styled to match Readest's main window — macOS
decorations + overlay title bar; other desktops decorationless with a
drop shadow; native background + in-page loading overlay pick up the
caller's `themeCode.bg`/`fg` so light/dark/eink/custom themes all
render correctly. Title localised, all five overlay/title strings
translated across 33 locales.
Notes
- Gates the macOS traffic-light positioner to main/reader-* windows so
the decorationless clip window no longer null-derefs in
`position_traffic_lights`.
- Stricter validation across the path: schemes restricted to http/https,
hex-color parsing rejects malformed values, server endpoint returns
400 on missing/invalid base64.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
73 lines
1.9 KiB
TypeScript
73 lines
1.9 KiB
TypeScript
import DOMPurify from 'dompurify';
|
|
|
|
/**
|
|
* Strip untrusted HTML (from email bodies, web pages, DOCX conversion) down to
|
|
* safe, EPUB-appropriate structural markup. Removes scripts, event handlers,
|
|
* styles, iframes, and form controls; keeps headings, text, lists, tables,
|
|
* links and images.
|
|
*
|
|
* Runs against the real DOM — Send to Readest converts on the client (browser
|
|
* or Tauri webview), both of which provide `window`/`DOMParser`.
|
|
*/
|
|
export function sanitizeHtml(html: string): string {
|
|
return DOMPurify.sanitize(html, {
|
|
ALLOWED_TAGS: [
|
|
'h1',
|
|
'h2',
|
|
'h3',
|
|
'h4',
|
|
'h5',
|
|
'h6',
|
|
'p',
|
|
'br',
|
|
'hr',
|
|
'blockquote',
|
|
'pre',
|
|
'code',
|
|
'strong',
|
|
'em',
|
|
'b',
|
|
'i',
|
|
'u',
|
|
's',
|
|
'sup',
|
|
'sub',
|
|
'span',
|
|
'ul',
|
|
'ol',
|
|
'li',
|
|
'dl',
|
|
'dt',
|
|
'dd',
|
|
'table',
|
|
'thead',
|
|
'tbody',
|
|
'tr',
|
|
'th',
|
|
'td',
|
|
'a',
|
|
'img',
|
|
'figure',
|
|
'figcaption',
|
|
],
|
|
// `id` is allowed so heading anchors survive — the EPUB's nested
|
|
// navMap uses `chapter1.xhtml#heading-id` to link the TOC sidebar to
|
|
// each section. Without it readers see one entry per chapter only.
|
|
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'colspan', 'rowspan', 'id'],
|
|
// Drop anything that would load or run remote code.
|
|
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input'],
|
|
FORBID_ATTR: ['srcset'],
|
|
ALLOW_DATA_ATTR: false,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Strip scripts and event handlers from an untrusted *document* before it is
|
|
* handed to `DOMParser`. Unlike `sanitizeHtml`, this keeps the document
|
|
* structure (`<head>`, `<title>`, sectioning elements) so title extraction and
|
|
* Readability still work — it only removes anything executable.
|
|
*/
|
|
export function sanitizeForParsing(html: string): string {
|
|
return DOMPurify.sanitize(html, { WHOLE_DOCUMENT: true });
|
|
}
|