A pure-JavaScript reader for Microsoft Compiled HTML Help (.chm) files — like PDF.js, but for .chm.
No plugins, no native modules, no WASM. It parses the ITSS/ITSF container and
decompresses LZX entirely in JavaScript, then renders each Help topic in a
strictly sandboxed iframe.
https://alpaq92.github.io/CHMate/ — open a .chm from your machine — it is
parsed locally and never leaves the browser — or load the bundled PuTTY manual.
Deployed automatically from main by GitHub Pages
(.github/workflows/pages.yml remains as a
manual fallback).
- Pure JS — ITSF/ITSP/PMGL container parser and an LZX decompressor with ResetTable seeking, all in plain ES modules. No build step, no dependencies.
- Faithful decoding — validated byte-for-byte against 13 real
.chmfiles (1,400+ internal files), including PuTTY's 270 KB manual and Windows system Help files. - Modern reader UI — Contents / Index / Files sidebar, history (back/forward), zoom, find-in-page, print, drag-and-drop, keyboard shortcuts. The theme toggle (light / dark / follow the OS) drives the whole app — toolbar, sidebar and the document area alike.
- Security first — every untrusted topic is sanitized and rendered inside a
fully
sandboxed iframe with a strict Content-Security-Policy; scripts are stripped out, internal resources are served from in-memoryblob:URLs, and the network is blocked so a hostile CHM can't phone home. - Works offline and as a tiny library or CLI in Node.
ES modules can't load over file://, so use the bundled dev server:
node tools/serve.mjs # → http://localhost:8080
# or: npm startThen open http://localhost:8080 and pick a .chm.
import { ChmReader } from './src/chm/chm-reader.js';
const reader = ChmReader.open(arrayBuffer); // Uint8Array or ArrayBuffer
reader.title; // "PuTTY User Manual"
reader.defaultTopic; // "/index.html"
reader.listFiles(); // ["/index.html", "/chapter1.html", ...]
reader.getFile(path); // Uint8Array
reader.getText(path); // decoded with the right charset
reader.getContents(); // table-of-contents tree (from the .hhc)
reader.getIndex(); // index tree (from the .hhk)node cli.mjs info file.chm
node cli.mjs list file.chm
node cli.mjs toc file.chm
node cli.mjs cat file.chm /index.html
node cli.mjs extract file.chm ./outThe only genuinely hard part of CHM is LZX decompression. CHMate's LZX decoder is an original implementation written from scratch against Microsoft's published [MS-PATCH] LZX specification — all of CHMate is original MIT code, and no third-party decoder is shipped. See CREDITS.md.
npm test # validates samples/*.chm by self-consistency
node test/run.mjs path/to/other.chmCHM files carry no checksums, so the suite proves correctness by self-consistency: every entry must decompress to exactly its declared length and, where the type is known, carry valid magic bytes / text. Any LZX error turns this into garbage that fails instantly.
CHM is a legacy and an active malware vector. CHMate treats every topic as hostile:
- rendered in
<iframe sandbox="allow-same-origin">— noallow-scripts, so JavaScript from the help file never executes at all; - a strict CSP (
default-src 'none'; onlyblob:/data:for images, styles, fonts) blocks all network access; <script>, event-handler attributes andjavascript:/vbscript:/ms-its:URLs are stripped;- internal images/stylesheets are rewritten to in-memory
blob:URLs sourced from inside the CHM; externalurl()s in CSS are dropped; links are intercepted by the host and external ones require confirmation; - nested
<frame>/<iframe>content is itself recursively sanitized and given its own CSP (never loaded as raw HTML), so a frameset can't smuggle in a network beacon.
MIT — see LICENSE and CREDITS.md.
