mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
book(I3): \{toc}\ token — auto-generated table of contents
A standalone \`{toc}\` line in any chapter's markdown expands at
pagination time into a list of every reflow chapter's H1 / H2 / H3
headings paired with their physical page number.
Two-pass internally:
- _computeBookTocEntries(book) walks the manifest and paginates
each reflow chapter with { _skipToc: true } so the inner pass
treats {toc} as an empty placeholder, then collects every
heading block plus the page number it lands on. Driven off the
same numbering rules enumerateBookPhysicalPages uses
(startAt / firstNumberedPage / skipNumber / skipNumberOnFirstPage).
- paginateChapter, when called without _skipToc and the markdown
contains a standalone \`{toc}\` line, replaces that line with the
generated TOC markdown before lexing. The TOC chapter itself
paginates normally with the expanded content.
- markdownToSafeHtml does the same substitution so the chapter
editor preview pane shows the rendered TOC as the user types
around it.
Known v1 limitation:
- TOC page numbers come from the pre-expansion enumeration; if
the TOC itself takes N pages, downstream chapter starts shift
by ~N from what the TOC reports. For typical books this is a
1–2 page offset. A full converging iteration is a follow-up
(acceptable v1 trade-off so we don't spin paginating the same
book repeatedly on every render).
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
e5721b67a7
commit
d16c475dca
1 file changed
+78
-3
+78
-3
@@ -9860,14 +9860,81 @@
|
||||
// list + hr layout with greedy line breaking. Inline runs
|
||||
// (bold / italic / inline-code) collapse to plain text using
|
||||
// the block's base style; Phase I gives them per-run styles.
|
||||
function paginateChapter(entry, book) {
|
||||
// Walk every reflow chapter, paginate without TOC, collect every
|
||||
// H1/H2/H3 heading + its physical page number. Used to expand
|
||||
// the `{toc}` magic token (I3) — two-pass so the TOC chapter's
|
||||
// own pagination doesn't recurse.
|
||||
function _computeBookTocEntries(book) {
|
||||
if (!book) return [];
|
||||
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;
|
||||
const entries = [];
|
||||
(book.pages || []).forEach((p, manifestIdx) => {
|
||||
const baseNumbered = enabled && !p.skipNumber && manifestIdx >= firstNumberedIdx;
|
||||
if (p.type === 'reflow') {
|
||||
let pag;
|
||||
try { pag = paginateChapter(p, book, { _skipToc: 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;
|
||||
for (const block of (page.blocks || [])) {
|
||||
const depthMatch = /^h([1-3])$/.exec(String(block.kind || ''));
|
||||
if (!depthMatch) continue;
|
||||
const text = (block.lines || []).join(' ').trim();
|
||||
if (!text) continue;
|
||||
entries.push({
|
||||
depth: Number(depthMatch[1]),
|
||||
text,
|
||||
pageNumber,
|
||||
entryId: p.id,
|
||||
chapterTitle: p.chapterTitle || '',
|
||||
});
|
||||
}
|
||||
if (numbered) printedNum++;
|
||||
});
|
||||
} else if (baseNumbered) {
|
||||
printedNum++;
|
||||
}
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
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', ''];
|
||||
for (const e of entries) {
|
||||
const indent = ' '.repeat(Math.max(0, (e.depth || 1) - 1));
|
||||
const page = (e.pageNumber != null) ? ` — Page ${e.pageNumber}` : '';
|
||||
lines.push(`${indent}- ${e.text}${page}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function paginateChapter(entry, book, opts = {}) {
|
||||
const result = { pages: [], overflow: [], warnings: [], masterId: null, frame: null };
|
||||
if (!entry || entry.type !== 'reflow') return result;
|
||||
if (typeof window.marked === 'undefined' || !window.marked.lexer) {
|
||||
result.warnings.push('marked library failed to load');
|
||||
return result;
|
||||
}
|
||||
const md = String(entry.chapterMarkdown || '');
|
||||
let md = String(entry.chapterMarkdown || '');
|
||||
// {toc} expansion (I3). Lines that are exactly `{toc}` (possibly
|
||||
// with whitespace) get replaced by an auto-generated TOC of every
|
||||
// H1/H2/H3 in the book + their page numbers. The recursive
|
||||
// call uses _skipToc:true so the inner pagination treats the
|
||||
// token as a placeholder.
|
||||
if (!opts._skipToc && /^[ \t]*\{toc\}[ \t]*$/m.test(md)) {
|
||||
const tocMd = _formatBookTocMarkdown(_computeBookTocEntries(book));
|
||||
md = md.replace(/^[ \t]*\{toc\}[ \t]*$/gm, tocMd);
|
||||
} else if (opts._skipToc) {
|
||||
// Pass 1: strip the placeholder line entirely so it doesn't
|
||||
// distort the headings' page numbers via a "{toc}" paragraph.
|
||||
md = md.replace(/^[ \t]*\{toc\}[ \t]*\n?/gm, '');
|
||||
}
|
||||
const styles = (book?.spec?.textStyles) || defaultBookTextStyles();
|
||||
const masterId = (typeof effectiveMasterIdForPage === 'function')
|
||||
? effectiveMasterIdForPage(entry)
|
||||
@@ -11553,7 +11620,15 @@
|
||||
return '<p>(marked library failed to load)</p>';
|
||||
}
|
||||
try {
|
||||
const html = window.marked.parse(String(src || ''), {
|
||||
// Expand {toc} so the chapter-editor preview pane shows what
|
||||
// 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);
|
||||
}
|
||||
const html = window.marked.parse(md, {
|
||||
mangle: false,
|
||||
headerIds: false,
|
||||
breaks: false,
|
||||
|
||||
Reference in new issue
Block a user