diff --git a/src/index.html b/src/index.html index 9ac0e43..8d2c512 100644 --- a/src/index.html +++ b/src/index.html @@ -9903,6 +9903,58 @@ }); return entries; } + // Slugify a heading / title into a lowercase-hyphen anchor — + // matches the GitHub-style #my-heading convention authors + // expect when writing cross-references. + function _bookSlugify(s) { + return String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); + } + // Build a slug → page-number map for cross-reference resolution + // (I6). Chapter titles, "chapter-N" aliases, and every H1-H6 + // heading register. Uses _skipXref:true on inner pagination so + // the map computation can't recurse. + function _computeBookSlugToPageMap(book) { + if (!book) return new Map(); + const map = new Map(); + const numbering = book.spec?.pageNumbering || {}; + const enabled = numbering.enabled !== false; + const startAt = Number.isFinite(numbering.startAt) ? numbering.startAt : 1; + const firstNumberedIdx = (Number.isFinite(numbering.firstNumberedPage) ? numbering.firstNumberedPage : 1) - 1; + let printedNum = startAt; + (book.pages || []).forEach((p, manifestIdx) => { + const baseNumbered = enabled && !p.skipNumber && manifestIdx >= firstNumberedIdx; + if (p.type === 'reflow') { + let pag; + try { pag = paginateChapter(p, book, { _skipXref: true }); } + catch { pag = { pages: [] }; } + (pag.pages || []).forEach((page, idxInChapter) => { + const skipThis = baseNumbered && p.skipNumberOnFirstPage && idxInChapter === 0; + const numbered = baseNumbered && !skipThis; + const pageNumber = numbered ? printedNum : null; + if (idxInChapter === 0 && pageNumber != null) { + if (p.chapterTitle) { + const slug = _bookSlugify(p.chapterTitle); + if (slug) map.set(slug, pageNumber); + } + if (p.chapterNumber != null) { + map.set(`chapter-${p.chapterNumber}`, pageNumber); + } + } + if (pageNumber != null) { + for (const block of (page.blocks || [])) { + if (!/^h[1-6]$/.test(String(block.kind || ''))) continue; + const slug = _bookSlugify((block.lines || []).join(' ')); + if (slug && !map.has(slug)) map.set(slug, pageNumber); + } + } + if (numbered) printedNum++; + }); + } else if (baseNumbered) { + printedNum++; + } + }); + return map; + } function _formatBookTocMarkdown(entries) { if (!entries.length) return '*No headings yet — add `# Heading` lines to your chapters and the TOC will fill in.*'; const lines = ['## Table of Contents', '']; @@ -9935,6 +9987,27 @@ // distort the headings' page numbers via a "{toc}" paragraph. md = md.replace(/^[ \t]*\{toc\}[ \t]*\n?/gm, ''); } + // Cross-reference substitution (I6). Markdown links pointing + // at an in-book anchor (`](#chapter-3)` etc) get their link + // text augmented with the destination's page number — slug + // matched against the same slugifier the build-time map uses. + // _skipXref:true short-circuits recursion from the map builder. + if (!opts._skipXref && /\]\(#[^)]+\)/.test(md)) { + try { + const xrefMap = _computeBookSlugToPageMap(book); + if (xrefMap.size) { + md = md.replace(/\[([^\]]*)\]\(#([^)]+)\)/g, (whole, text, href) => { + const slug = _bookSlugify(href); + const page = xrefMap.get(slug); + if (page != null && text) return `[${text} (p. ${page})](#${href})`; + if (page != null) return `[p. ${page}](#${href})`; + return whole; + }); + } + } catch (err) { + console.warn('xref substitution failed', err); + } + } const styles = (book?.spec?.textStyles) || defaultBookTextStyles(); const masterId = (typeof effectiveMasterIdForPage === 'function') ? effectiveMasterIdForPage(entry) @@ -11717,9 +11790,25 @@ // the rendered chapter will look like with the auto-generated // table of contents in place. let md = String(src || ''); - if (state.templateMode === 'book' && state.book && /^[ \t]*\{toc\}[ \t]*$/m.test(md)) { - const tocMd = _formatBookTocMarkdown(_computeBookTocEntries(state.book)); - md = md.replace(/^[ \t]*\{toc\}[ \t]*$/gm, tocMd); + if (state.templateMode === 'book' && state.book) { + if (/^[ \t]*\{toc\}[ \t]*$/m.test(md)) { + const tocMd = _formatBookTocMarkdown(_computeBookTocEntries(state.book)); + md = md.replace(/^[ \t]*\{toc\}[ \t]*$/gm, tocMd); + } + // Cross-reference substitution in the live preview too. + if (/\]\(#[^)]+\)/.test(md)) { + try { + const xrefMap = _computeBookSlugToPageMap(state.book); + if (xrefMap.size) { + md = md.replace(/\[([^\]]*)\]\(#([^)]+)\)/g, (whole, text, href) => { + const page = xrefMap.get(_bookSlugify(href)); + if (page != null && text) return `[${text} (p. ${page})](#${href})`; + if (page != null) return `[p. ${page}](#${href})`; + return whole; + }); + } + } catch {} + } } const html = window.marked.parse(md, { mangle: false,