this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix TOC-to-chapter navigation mismatch

The table of contents display now shows inline chapter references [ch: N]
so users know exactly which number to use with the chapter command.

Previously, TOC item numbers did not correspond to spine-based chapter
indices, making it confusing to navigate books with nested TOC structures.

Changes:
- Add buildHrefToSpineMap() to create href-to-spine index mapping
- Add findTocItemByHref() to recursively search TOC tree
- Update formatToc() to display [ch: N] inline with each entry
- Fix getChapterContent() to use proper TOC lookup for titles
- Update documentation (SKILL.md, README.md, AGENTS.md)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

TKTK 5d901253 f2828775

+112 -14
+17
AGENTS.md
··· 68 68 ``` 69 69 70 70 Restart Claude Code after any changes to SKILL.md for them to take effect. 71 + 72 + ## Changelog 73 + 74 + ### 2025-11-26: TOC-to-Chapter Navigation Fix 75 + 76 + **Problem:** The table of contents (TOC) display showed sequential numbering that didn't correspond to the `chapter` command's spine-based indexing. Users had no way to know which chapter number to use. 77 + 78 + **Solution:** 79 + - TOC now displays inline chapter references: `Chapter Five [ch: 14]` 80 + - Users can see exactly which number to use with the `chapter` command 81 + - Fixed title extraction to properly search the nested TOC tree by href instead of assuming index alignment 82 + 83 + **Changes made to `src/index.ts`:** 84 + 1. Added `buildHrefToSpineMap()` - creates href-to-spine index mapping 85 + 2. Added `findTocItemByHref()` - recursively searches TOC tree for matching href 86 + 3. Updated `formatToc()` - shows `[ch: N]` inline with each entry 87 + 4. Fixed `getChapterContent()` - uses proper TOC lookup for title extraction
+1 -1
README.md
··· 5 5 ## Features 6 6 7 7 - **Metadata extraction** - title, author, publisher, date, language 8 - - **Table of contents** - view chapter structure 8 + - **Table of contents** - view chapter structure with chapter references (`[ch: N]`) 9 9 - **Chapter reading** - read specific chapters by number 10 10 - **Full extraction** - extract entire book as markdown 11 11 - **Search** - find text with surrounding context
+2 -1
SKILL.md
··· 22 22 ``` 23 23 24 24 #### 2. List Table of Contents 25 - View all chapters and their structure. 25 + View all chapters and their structure. Each entry shows `[ch: N]` indicating the chapter number to use with the `chapter` command. 26 26 27 27 ```bash 28 28 node ~/.claude/skills/epub/scripts/epub-reader/dist/index.js toc "<path-to-epub>" ··· 83 83 ## Notes 84 84 85 85 - Chapter numbers are 1-indexed (first chapter is 1, not 0) 86 + - Use the `[ch: N]` reference from the TOC output to find the correct chapter number 86 87 - Paths with spaces must be quoted 87 88 - Large books may produce substantial output with the `full` command 88 89 - Search results show up to 5 matches per chapter with context
+44 -6
scripts/epub-reader/dist/index.js
··· 189 189 if (!content) { 190 190 throw new Error(`Could not read content file: ${fullPath}`); 191 191 } 192 - // Extract title from content if possible 192 + // Extract title: search TOC tree for matching href, fallback to content extraction 193 193 const titleMatch = content.match(/<title>([^<]*)<\/title>/i); 194 194 const h1Match = content.match(/<h1[^>]*>([^<]*)<\/h1>/i); 195 - const title = epub.toc[index]?.label || 195 + const tocItem = findTocItemByHref(epub.toc, manifestItem.href); 196 + const title = tocItem?.label || 196 197 h1Match?.[1] || 197 198 titleMatch?.[1] || 198 199 `Chapter ${index + 1}`; ··· 230 231 function escapeRegex(string) { 231 232 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 232 233 } 233 - function formatToc(toc, indent = 0) { 234 + /** 235 + * Build a map from href (normalized, without fragment) to spine index (1-based). 236 + */ 237 + function buildHrefToSpineMap(epub) { 238 + const hrefToSpine = new Map(); 239 + epub.spine.forEach((spineItem, index) => { 240 + const manifestItem = epub.manifest.get(spineItem.idref); 241 + if (manifestItem) { 242 + // Normalize href: remove fragment and leading path components that might differ 243 + const href = manifestItem.href.split("#")[0]; 244 + hrefToSpine.set(href, index + 1); // 1-based for user display 245 + } 246 + }); 247 + return hrefToSpine; 248 + } 249 + /** 250 + * Recursively search TOC tree for an item matching the given href. 251 + */ 252 + function findTocItemByHref(toc, href) { 253 + const normalizedHref = href.split("#")[0]; 254 + for (const item of toc) { 255 + const itemHref = item.href.split("#")[0]; 256 + if (itemHref === normalizedHref) { 257 + return item; 258 + } 259 + if (item.children) { 260 + const found = findTocItemByHref(item.children, href); 261 + if (found) 262 + return found; 263 + } 264 + } 265 + return undefined; 266 + } 267 + function formatToc(toc, hrefToSpine, indent = 0) { 234 268 let output = ""; 235 269 toc.forEach((item, index) => { 236 270 const prefix = " ".repeat(indent); 237 - output += `${prefix}${indent === 0 ? index + 1 + "." : "-"} ${item.label}\n`; 271 + const itemHref = item.href.split("#")[0]; 272 + const spineIndex = hrefToSpine.get(itemHref); 273 + const chapterRef = spineIndex ? ` [ch: ${spineIndex}]` : ""; 274 + output += `${prefix}${indent === 0 ? index + 1 + "." : "-"} ${item.label}${chapterRef}\n`; 238 275 if (item.children) { 239 - output += formatToc(item.children, indent + 1); 276 + output += formatToc(item.children, hrefToSpine, indent + 1); 240 277 } 241 278 }); 242 279 return output; ··· 287 324 .action(async (file) => { 288 325 try { 289 326 const epub = await loadEpub(file); 327 + const hrefToSpine = buildHrefToSpineMap(epub); 290 328 console.log("# Table of Contents\n"); 291 329 if (epub.toc.length > 0) { 292 - console.log(formatToc(epub.toc)); 330 + console.log(formatToc(epub.toc, hrefToSpine)); 293 331 } 294 332 else { 295 333 // Fallback to spine-based listing
+48 -6
scripts/epub-reader/src/index.ts
··· 299 299 throw new Error(`Could not read content file: ${fullPath}`); 300 300 } 301 301 302 - // Extract title from content if possible 302 + // Extract title: search TOC tree for matching href, fallback to content extraction 303 303 const titleMatch = content.match(/<title>([^<]*)<\/title>/i); 304 304 const h1Match = content.match(/<h1[^>]*>([^<]*)<\/h1>/i); 305 + const tocItem = findTocItemByHref(epub.toc, manifestItem.href); 305 306 const title = 306 - epub.toc[index]?.label || 307 + tocItem?.label || 307 308 h1Match?.[1] || 308 309 titleMatch?.[1] || 309 310 `Chapter ${index + 1}`; ··· 357 358 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 358 359 } 359 360 360 - function formatToc(toc: TocItem[], indent = 0): string { 361 + /** 362 + * Build a map from href (normalized, without fragment) to spine index (1-based). 363 + */ 364 + function buildHrefToSpineMap(epub: ParsedEpub): Map<string, number> { 365 + const hrefToSpine = new Map<string, number>(); 366 + 367 + epub.spine.forEach((spineItem, index) => { 368 + const manifestItem = epub.manifest.get(spineItem.idref); 369 + if (manifestItem) { 370 + // Normalize href: remove fragment and leading path components that might differ 371 + const href = manifestItem.href.split("#")[0]; 372 + hrefToSpine.set(href, index + 1); // 1-based for user display 373 + } 374 + }); 375 + 376 + return hrefToSpine; 377 + } 378 + 379 + /** 380 + * Recursively search TOC tree for an item matching the given href. 381 + */ 382 + function findTocItemByHref(toc: TocItem[], href: string): TocItem | undefined { 383 + const normalizedHref = href.split("#")[0]; 384 + 385 + for (const item of toc) { 386 + const itemHref = item.href.split("#")[0]; 387 + if (itemHref === normalizedHref) { 388 + return item; 389 + } 390 + if (item.children) { 391 + const found = findTocItemByHref(item.children, href); 392 + if (found) return found; 393 + } 394 + } 395 + return undefined; 396 + } 397 + 398 + function formatToc(toc: TocItem[], hrefToSpine: Map<string, number>, indent = 0): string { 361 399 let output = ""; 362 400 toc.forEach((item, index) => { 363 401 const prefix = " ".repeat(indent); 364 - output += `${prefix}${indent === 0 ? index + 1 + "." : "-"} ${item.label}\n`; 402 + const itemHref = item.href.split("#")[0]; 403 + const spineIndex = hrefToSpine.get(itemHref); 404 + const chapterRef = spineIndex ? ` [ch: ${spineIndex}]` : ""; 405 + output += `${prefix}${indent === 0 ? index + 1 + "." : "-"} ${item.label}${chapterRef}\n`; 365 406 if (item.children) { 366 - output += formatToc(item.children, indent + 1); 407 + output += formatToc(item.children, hrefToSpine, indent + 1); 367 408 } 368 409 }); 369 410 return output; ··· 413 454 .action(async (file: string) => { 414 455 try { 415 456 const epub = await loadEpub(file); 457 + const hrefToSpine = buildHrefToSpineMap(epub); 416 458 417 459 console.log("# Table of Contents\n"); 418 460 419 461 if (epub.toc.length > 0) { 420 - console.log(formatToc(epub.toc)); 462 + console.log(formatToc(epub.toc, hrefToSpine)); 421 463 } else { 422 464 // Fallback to spine-based listing 423 465 console.log("(No structured TOC found, listing spine items)\n");