mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
book(I6): cross-references — [text](#chapter-N) → page numbers
Markdown links pointing at in-book anchors get their link text
augmented with the destination's page number at pagination time.
Slug matching:
- _bookSlugify(s) = lowercase + non-alphanumeric → hyphen, trim
edge dashes (matches GitHub-style heading-anchor convention).
- _computeBookSlugToPageMap(book) walks every reflow chapter with
_skipXref:true to avoid recursion, building a slug → page-number
map that registers:
* Each chapter's slugified chapterTitle.
* "chapter-N" aliases for entries with chapterNumber.
* Each H1-H6 heading's slugified text (first occurrence wins).
Substitution sites:
- paginateChapter: when the chapter markdown contains `](#anchor)`
and !opts._skipXref, rewrites every matching link to include
" (p. N)" in its visible text. Renders through DOM tile thumbs,
Reader Mode, and PDF real-text emission the same way.
- markdownToSafeHtml: live preview pane gets the same rewrite so
the author sees page numbers as they type.
- Unknown slugs pass through unchanged so a typo'd anchor stays
visible as a regular markdown link instead of silently breaking.
Phase I now has every spec'd feature except I4 (footnotes) and I5
(auto-index from inline ^[term] markers) shipped end-to-end.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
4b2f124fde
commit
f3d8135a9a
1 file changed
+92
-3
+92
-3
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user