| name | html-book-bundler |
| description | Master Skill Bundle (v9.0). A 4-role pipeline (Ingest, Architect, Design, Assemble) for creating offline-first interactive books with high-fidelity UI. |
HTML Book Bundler (Skill Bundle v9.0)
This is the umbrella skill for the book production toolchain. It manages 4 specialized sub-skills for each layer of production.
This bundle enables the creation of autonomous, interactive, offline-first HTML books delivered as a single, dependency-free bundle. It transforms raw manuscripts into high-fidelity reading applications that function 100% without a server or internet connection.
MANDATORY EXECUTION PROTOCOL: THE DIRECTOR'S CUT (V9.0)
You are an AI Architect and Book Director. Your goal is to transform a boring PDF into a high-fidelity, interactive learning experience.
- STEP 1: SMART INGEST & CLEAN. Run
ingest.py --strip-noise and generate_layout_map.py. You MUST strip PDF artifacts (headers, footers) and understand the topology.
- If ingest prints
[NOTICE] PDF is a scan. Pages exported to assets/raw_scans/., switch to Visual OCR Mode: read pages from assets/raw_scans/ (via media-reading tools) and recover structure + visuals, not plain text only.
- STEP 2: STORYBOARD & DIRECTOR'S NOTE. For each batch (3-5 chapters), you MUST first write a "Director's Note" in the chat:
- Sanitization Plan: What noise will be removed.
- Style Proposal: Book-specific palette and vibe (colors, typography tone, spacing rhythm) based on the first pages/cover analysis.
- Visual Transformation: Which text blocks become
.card, .formula-card, or .vis-diag.
- Interactive Elements: Plan for SVGs and Accordions.
- Per-Chapter Pattern Map: For each chapter in the batch, specify the pattern from
references/visual-bank.md (for example: Translator, Gauge, Timeline).
- Wait for user approval of the Storyboard.
- STEP 3: RICH DESIGN & VERIFY. Physically modify chapters using the Component Library.
- Formula Cards: Wrap ALL technical definitions in
.formula-card.
- SVG Primitives: Use SVG schemes for logic flows.
- Scroll Guard: Ensure
body has overflow-y: auto !important.
- STEP 4: MODULAR ASSEMBLY & AUDIT. Use
default.html (the only production-ready interactive shell). Bundle, then verify with Playwright using the Interactive Audit Protocol.
CORE MANDATES:
- Scoped CSS Rule: NEVER style global
html or body tags in chapters. All styles MUST be scoped under the .chapter-content selector to prevent breaking the shell's UI (search, navigation, themes).
- Local-First Assets: Prioritize downloading external images (Unsplash, Wikimedia) to a local
assets/ folder before bundling. Avoid external URLs in production to prevent 403/404 errors.
- Visual Literacy: If a chapter has >500 words without a visual component (SVG, Card, Table, or Photo), it is a FAILURE.
- Rule 70/30 (Acceptance Gate): At least 70% of chapter flow must be structured/visual components and up to 30% plain prose.
- No "Bicycle" Templates: Always use the official
bundle.cjs and default.html. Customization is allowed ONLY via theme.css or component injection.
- Designer Rule: "Inherit the base, create the unique." Start from
assets/theme.css variables and override only what's necessary for the genre.
- Zero Hex Colors: All components must be theme-adaptive.
Interactive Audit Protocol (Mandatory):
After assembly, run Playwright and verify:
- Scroll Check:
window.scrollTo must actually change window.scrollY.
- Search Check: Typing a known word must produce
mark.search-hit elements.
- Image Check:
img.naturalWidth must be > 0.
Critical Lessons (v9.0 Additions):
- Scroll Guard: The
srcdoc iframe environment requires explicit overflow: auto on the chapter body. Never use overflow: hidden.
- Unicode LaTeX: In Python enrichment scripts, always use raw strings (
r"") and double-escape backslashes (\\\\) to avoid SyntaxError.
- Sanitization Priority: Raw parsing is never enough. Removing headers and footers is as important as extracting text.
- MathJax Integration: The
default.html now includes MathJax by default. Chapters just need to provide \( ... \) or $$ ... $$.
Components (Sub-skills):
- book-ingester: Unified extraction from PDF, EPUB, FB2, DOCX (including images).
- book-architect: Semantic distillation,
terms.json mining, and visual blueprinting.
- book-designer: AST-based enrichment, high-fidelity SVGs, and contrast enforcement.
- book-assembler: Final bundling with stable MD5 indexing and security audit.
Shared Mantras:
- Zero Hex Colors: SVGs must be theme-adaptive via CSS variables.
- Variables Contract: Both the shell (
default.html) and chapters (theme.css) MUST use identical CSS variable names (--bg, --txt, --acc, --acc2, --line, --muted, --warn, --bad, --bg2, --fg, --panel, --panel2) to ensure seamless visual integration.
- Stable Identity:
BOOK_ID is derived from the book title (MD5 hash). Changing the --title resets user progress; changing the output filename does NOT.
- Cheerio over Regex: All structural HTML modifications MUST use Cheerio AST parsing for reliability.
- Mobile First: All output must be responsive, respect safe-area-insets, and allow pinch-to-zoom (WCAG 1.4.4).
Critical Lessons (2026-04-17):
- Sandbox Resolution (FIXED in v8.3): The iframe now uses ONLY
allow-scripts — allow-same-origin has been removed. All shell/chapter communication is done via postMessage (theme, scroll, anchors, search, asset loading). Never reintroduce allow-same-origin; never call window.parent.document from chapter JS. The CSP connect-src: none remains as a defense-in-depth layer.
- Base64 Bloat: Limit image width to 1000px in
optimize_assets.py to prevent mobile browser OOM crashes. The function default AND the CLI default MUST both be 1000 — verify if changing either.
- EPUB Navigation: Regex path rewriting must skip
.html/.xhtml/# to preserve inter-chapter links.
- PDF Extraction: Use dynamic font baseline calculation (
_calculate_baseline) for relative heading detection (Pocket vs A4 formats). Without --pdf-chapters, the entire PDF becomes ONE chapter.
- Data-URI Memory Management: Injecting hundreds of megabytes of Base64 strings directly into the DOM crashes mobile Safari. Move assets to a JSON dictionary and lazy load them via
IntersectionObserver in the guest script.
- Standard API over Browser Hacks: Relying on
window.find() creates inconsistent search UI and doesn't allow scrolling through multiple hits per chapter. Custom TreeWalker highlighting (<mark>) is the correct approach.
- Hardcoded Logic Breaks i18n: Stop words for inverted indices should not be hardcoded in the bundler. They are language-specific and must be loaded dynamically from
lang/*.json files.
- QA Linter Hex Expansion: Linter checking SVGs for hardcoded hex codes must support 8-character codes (
#RRGGBBAA) exported by Figma, not just 3 or 6 characters.
- Text-to-HTML XSS: When converting list items to stat cards or grid cards, always HTML-escape
.text() values before interpolating into template literals. Cheerio .text() decodes entities — re-injecting them raw creates broken HTML.
- Sub-skill SKILL.md must be self-contained: Each role's SKILL.md is the full context for the AI agent executing that role. Never put critical lessons only in the umbrella SKILL.md — the sub-agent won't see it.
Critical Lessons (2026-04-18, audit session):
- Search Highlighting vs Tag Boundaries: Simple text-node search fails on formatted words (e.g.,
<b>W</b>ord). Use a virtual text buffer + DOM mapping to ensure robust search across tag boundaries.
- Scanned PDF Validation: Always check the total extracted text length. If 0, warn the user explicitly about "Scanned PDF" to prevent confusion over empty outputs.
- Unified JSON Injection: Use a dedicated
safeJsonInject helper for all JSON blobs in the template. This centralizes </script> escaping and prevents breaking the output file with embedded code examples.
- Theme Variable Integrity: Validate the presence of mandatory CSS variables (
--bg, --acc, etc.) during bundling. This prevents a "broken UI" experience when a custom theme is incomplete.
guestReady is the only safe sync point: Never send setTheme, setScrollRatio, or highlightSearch from iframe.onload. The onload event fires before the guest script runs. Always wait for {action:'guestReady'} from the guest before sending any postMessage commands.
- Chapter Size Limit (Mobile OOM): Mobile browsers (especially iOS Safari) crash when an
iframe.srcdoc string exceeds ~2-3MB. MANDATORY: bundle.cjs must split any chapter exceeding 2,097,152 bytes (2MB) into multiple chunks (e.g., "Chapter 1.1", "Chapter 1.2"). This is handled at the bundling layer, not the ingestion layer.
Critical Lessons (2026-04-18, second audit):
- CSS must stay synchronized between
default.html and theme.css: Both files define the same visual layer (shell sidebar and chapter iframe). Whenever you update font-family, antialiasing, or any global CSS in default.html, make the identical change in theme.css. The comment /* Matches default.html shell font — always keep these in sync */ marks the critical line.
- EPUB
ingest_epub() must receive --lang: ingest_epub() previously lacked a lang parameter, causing chapters to inherit whatever lang was in the EPUB source (often wrong for localized EPUB editions). Always pass lang through to ingest_epub() and inject it into each chapter's <html> tag if missing.
- navScript regexes require double-double escaping: JS regex patterns inside CJS template literals need
\\\\d for \d, \\\\. for \., etc. Each backslash is doubled twice (once for the JS string literal, once for the regex engine). Do not edit navScript regex patterns without understanding this — one extra or missing slash silently breaks chapter navigation mapping.
- Tests and linter must cover SEARCH_IDX: Having tests only for CHAPTERS+ASSETS escaping is insufficient. Added Test 21 (SIDX structure + escaping) and a
lint_book.py SIDX validation. Without both, a broken SIDX disables all search with no visible error.
@media print is fundamentally broken for iframes: iframe content does not render in browser print layout. Replaced phantom print CSS with a print-btn that calls window.open() and restores lazy images from ASSETS before printing. The new popup prints correctly.
wrap() in Cheerio uses the innermost child: $p.wrap($details) inserted $p inside <summary> (the only child of $details), not after it. Use $p.replaceWith($details); $details.append($p) to get the correct sibling structure.
dev_server.cjs must exist if --dev is documented: The --dev flag injects an SSE live-reload client. dev_server.cjs is the matching server. Added a minimal stdlib-only implementation in scripts/dev_server.cjs.
Critical Lessons (2026-04-18, final offline-first audit):
- Search Tokenization Parity: Maintain exact parity in how punctuation is stripped between the build-time indexer (e.g.
bundle.cjs) and the client read-time search. Replacing punctuation with spaces (vs empty strings) preserves multi-word matching for hyphenated compounds. Target independent words rather than single contiguous substrings.
- Iframe Event Isolation (Focus Trap): The reader's focus naturally falls on the iframe. Keyboard events (
keydown) are swallowed. You must explicitly listen for core navigation keys (ArrowRight, ArrowLeft, Escape) within the guest iframe and postMessage them back to the shell window.
- Cheerio First Child Trap: When checking the structure of a DOM element using Cheerio,
.children() ignores Text nodes. If a user types text before a <b> tag, .children().first() will jump straight to the <b>, creating false positives for structural matches. Always use .contents() to check the true first node.
- JSON Script Escaping Trick: Escaping JSON inside an injected script by replacing
</script> with <\\/script> is a legitimate and necessary pattern for single-file HTML apps. Do not refactor it away even if linters complain.
- Cross-Chunk Anchor Resolution: Due to the 2MB split limit (Mobile OOM mitigation),
<a href="#footnote-1"> or <a href="chapter3.html#conclusion"> breaks if the target id was split into a different chunk from the origin/shell target. MANDATORY: splitChapter must extract all IDs globally into GLOBAL_ANCHORS mapped to the chunk index. The shell must attempt ANCHORS[anchorId] before searching for a file index, routing directly to the exact chunk.
- Local Anchor Escalation: In the
iframe guest script, if document.getElementById(anchor) fails, the anchor might reside in a sibling chunk. The script must emit postMessage({ action: 'bookGo', anchorId }) up to the shell instead of silently failing.
Critical Lessons (2026-04-18, final high-stakes audit):
- DOM Hierarchy Preservation in Split:
splitChapter MUST track open tags via a stack to avoid breaking CSS/layout. When splitting at 2MB, explicitly close all open tags at the chunk boundary and re-open them at the start of the next chunk (e.g. <main><div...> -> ...</div></main>).
- Parsing Freeze Mitigation (JSON Scripts): Never inject large data (SIDX, CHAPTERS, ASSETS) as JS object literals. This triggers eager V8 AST parsing which freezes the main thread on mobile. Inject into
<script type="application/json"> and use JSON.parse() on demand — it's 2-5x faster and avoids thread blocking.
- Sidebar Rendering Performance: Use
DocumentFragment when building large chapter/search result lists. For books with >300 chunks, batching DOM insertions prevents layout thrashing and ensures smooth UI interaction.
- Tokenization Integrity:
tokenize() must explicitly strip <script> and <style> blocks before removing other tags. Otherwise, JS logic and CSS rules are indexed as book content, polluting search results with code snippets.
- Asset Key Integrity: Include an MD5 hash of the asset content in the
asset_ key. This allows the guest script/shell to reliably identify duplicate assets across multiple chapters and reduces the total file size through deduplication.
- Iframe
srcdoc Length Hazards: While srcdoc is modern and safe, keep individual chunk sizes below 2MB. Some mobile engines silently fail to render or experience massive GC pauses when srcdoc receives a single contiguous string larger than 5MB. 2MB is the "Sweet Spot" for responsiveness.
Usage:
Refer to the individual SKILL.md in subdirectories for role-specific instructions.