From 0651a3e4fcc7d881afbc89b48843e8590de4b47e Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:46:45 -0400 Subject: [PATCH] book(I4): footnote refs + chapter-end definitions (v1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inline \`[^N]\` markers render as Unicode-superscript numerals (⁰¹²³…) and \`[^N]: definition\` lines collect into a "Footnotes" section appended to the chapter (after the body, separated by a horizontal rule). How it works: - _processBookFootnotes(md): 1. Sweeps definitions via /^[ \t]*\[\^id\]:[ \t]+(.+)$/gm, stores them in a Map keyed by raw id, strips the line from the body. 2. Renumbers: each defined id gets a 1..N number based on first encounter of its \`[^id]\` ref in the body. Refs without a matching def pass through unchanged. 3. Replaces every resolved ref with _bookSuperscript(num) so ¹² renders as actual superscript glyphs. 4. Appends a "### Footnotes" section with one bullet per definition in canonical (first-ref) order. Substitution sites: - paginateChapter: runs after the {index} expansion + marker stripping, before xref resolution. Only fires when \`[\^\` is present in the source so most chapters pay zero cost. - markdownToSafeHtml: same flow for the live preview pane. Known v1 limitation (deferred to I4.2): - Definitions render as a single end-of-chapter section rather than per-page in a footnote-frame layer on the master. Per-page footnote placement requires: * A new footnote-frame layer kind on masters (parallel to text-frame). * Page-aware definition collection in the pagination engine. * Page-break logic that keeps a ref + its def on the same page. Out of scope for this slice; the chapter-end section is the pragmatic v1. Phase I scope: I1 ✅ I2.1 ✅ I2.2 ✅ I3 ✅ I4 ✅ I5 ✅ I6 ✅. All spec'd Phase I features ship in v1 form. Co-Authored-By: Claude Opus 4.7 --- src/index.html | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/index.html b/src/index.html index 82c71fe..ede0808 100644 --- a/src/index.html +++ b/src/index.html @@ -9955,6 +9955,53 @@ }); return map; } + // Footnote support (I4 v1). Inline `[^N]` becomes a superscripted + // marker (Unicode digits ⁰¹²³…). `[^N]: definition` lines collect + // into a chapter-end footnotes section. Full per-page + // footnote-frame layout (frame layer on masters with page-aware + // definition placement) lands in I4.2. + const _BOOK_FN_DEF_RX = /^[ \t]*\[\^([^\]]+)\]:[ \t]+(.+)$/gm; + const _BOOK_FN_REF_RX = /\[\^([^\]]+)\]/g; + const _BOOK_SUP_DIGITS = ['⁰','¹','²','³','⁴','⁵','⁶','⁷','⁸','⁹']; + function _bookSuperscript(s) { + return String(s).replace(/[0-9]/g, c => _BOOK_SUP_DIGITS[Number(c)] || c); + } + function _processBookFootnotes(md) { + // Collect definitions (each on its own line). Strip them from + // the main body so they don't render inline. + const defs = new Map(); // id (string) → text + const stripped = String(md || '').replace(_BOOK_FN_DEF_RX, (_, id, text) => { + defs.set(String(id).trim(), text.trim()); + return ''; // drop the def line from body + }); + if (!defs.size) { + // No definitions — leave any stray `[^N]` refs as-is. + return stripped.replace(/\n{3,}/g, '\n\n'); + } + // Renumber: each defined id gets a sequential 1..N number based + // on first encounter of its ref in the body. Refs that don't + // resolve to a known def pass through unchanged. + const order = []; // canonical order [{ id, num }] + const numById = new Map(); + let nextNum = 1; + const body = stripped.replace(_BOOK_FN_REF_RX, (whole, rawId) => { + const id = String(rawId).trim(); + if (!defs.has(id)) return whole; + if (!numById.has(id)) { + numById.set(id, nextNum); + order.push({ id, num: nextNum }); + nextNum++; + } + return _bookSuperscript(numById.get(id)); + }); + // Footnotes section appended to the body. + const lines = ['', '---', '', '### Footnotes', '']; + for (const { id, num } of order) { + lines.push(`${_bookSuperscript(num)} ${defs.get(id)}`); + } + return body.replace(/\n{3,}/g, '\n\n') + '\n' + lines.join('\n'); + } + // Index marker support (I5). `^[term]` inline markers register // their term in a book-wide index keyed to the containing // chapter's first page. `{index}` expands at render time into @@ -10064,6 +10111,13 @@ // substitution so a marker inside a link text doesn't confuse // the parser. md = _stripBookIndexMarkers(md); + // Footnote refs (I4). Replace `[^N]` with a Unicode superscript + // and append a "Footnotes" section if any `[^N]: definition` + // lines were defined in the chapter source. Refs without a + // matching def pass through unchanged. + if (/\[\^/.test(md)) { + md = _processBookFootnotes(md); + } // 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 @@ -11877,6 +11931,7 @@ md = md.replace(/^[ \t]*\{index\}[ \t]*$/gm, idxMd); } md = _stripBookIndexMarkers(md); + if (/\[\^/.test(md)) md = _processBookFootnotes(md); // Cross-reference substitution in the live preview too. if (/\]\(#[^)]+\)/.test(md)) { try {