This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
pnpm build # Build both Tampermonkey userscript + Chrome extension
pnpm build:tamper # Build only Tampermonkey userscript → dist/tampermonkey/
pnpm build:ext # Build only Chrome/Edge extension → dist/extension/Package manager is pnpm@10. Node >=22.
This is a dual-target project that downloads PPT courseware from 学习通 (chaoxing.com) as PDF files. It produces both a Tampermonkey userscript and a Chrome/Edge extension from the same core logic.
Two tsup configs produce two IIFE bundles. Both inject the full jspdf.umd.min.js source at build time via process.env.JSPDF_SOURCE — this is needed because the PDF generation runs inside a Web Worker constructed from a Blob URL, where normal module imports aren't available.
tsup.tampermonkey.config.ts— bundlessrc/index.ts, prependsUserScript==metadata banner read frompackage.json.tampermonkeytsup.extension.config.ts— bundlessrc/extension/extension-entry.ts, generatesmanifest.json(Manifest V3), an inlinebackground.jsservice worker, copies the icon, and zips everything
Entry point (src/index.ts): XuexitongPPTDownloader class. On load it:
- Finds PPT iframes via
findPPTIframes()(recursive search for#panView, up to 5 levels deep, plus attribute-based fallback) - Mounts a
ButtonGroup(download + stop buttons) into each found iframe's document - Uses
WeakMap<HTMLIFrameElement, Document>to detect iframe navigations (new Document object → re-add buttons) - Listens for SPA route changes (
popstate/hashchange+ monkey-patchedhistory.pushState/replaceState) +MutationObserver+ a 10-retry timer
Download pipeline (triggered by button click):
ppt-extractor.ts— reads thedataattribute for the filename, waits for nested#panViewiframe to load, countsli[id^="anchor"]elements for page count, derivesbaseUrlfrom the first image URLpdf-generator.ts— downloads the first image to determine page dimensions, creates a Web Worker from the serializedpdfWorkerEntryfunction (+ injected jsPDF source), then concurrently downloads remaining images in batchesimage-downloader.ts— downloads viaGM_xmlhttpRequest(bypasses CORS), converts PNG→JPEG viaOffscreenCanvas/createImageBitmap, reports backArrayBufferobjects transferred to the Worker with zero-copypdf-worker.ts— receivesINIT/ADD_PAGE/FINISHmessages, calls jsPDF APIs, returns the final PDF Blob viapostMessage- Files are saved by creating a Blob URL and clicking a temporary
<a>element
Extension polyfill (src/extension/extension-polyfill.ts): In the extension build, GM_xmlhttpRequest is polyfilled using chrome.runtime.sendMessage to a background service worker that does fetch() and returns data: URLs. GM_getValue/GM_setValue are stubbed. The extension entry (extension-entry.ts) imports the polyfill before importing the main logic.
Download cancellation (src/download-controller.ts): Cooperative abort via throwIfAborted() checkpoints in the download loop. The stop button sets the flag, the loop breaks, and a DownloadAbortedError is caught.
- All image downloading MUST go through
GM_xmlhttpRequest(or the extension polyfill). Directfetch()/<img>won't work because PPT images are cross-origin (hosted oncldisk.com). - The
pdfWorkerEntryfunction is serialized with.toString()and executed in a Worker — it cannot reference any outer-scope variables. - Zero-copy transfer:
ArrayBufferobjects are transferred to the Worker via the transfer list (second argument topostMessage), so they become unusable on the main thread after sending. - Page limit: PDFs with >500 pages (configurable in
src/config.ts) trigger a split/merge prompt to avoidRangeError: Invalid string lengthfrom jsPDF's string concatenation.