From 47c403970651f7b82c4291cc965d2433da775c1c Mon Sep 17 00:00:00 2001 From: Thomas Walker Lynch Date: Mon, 1 Jun 2026 13:08:35 +0000 Subject: [PATCH] adds --- .../authored/RT/layout/paginate_by_element.js | 230 ++++++++++-------- ...manual.html => RT-semantic-HTML-tags.html} | 25 +- shared/authored/version | 2 +- 3 files changed, 156 insertions(+), 101 deletions(-) rename document/{style_manual.html => RT-semantic-HTML-tags.html} (94%) diff --git a/developer/authored/RT/layout/paginate_by_element.js b/developer/authored/RT/layout/paginate_by_element.js index 2955b3b..8c2dbb6 100644 --- a/developer/authored/RT/layout/paginate_by_element.js +++ b/developer/authored/RT/layout/paginate_by_element.js @@ -1,16 +1,15 @@ window.StyleRT.paginate_by_element = function () { const RT = window.StyleRT; + const debug = RT.debug || { log: function(){}, error: function(){} }; const page_conf = (RT.config && RT.config.page) ? RT.config.page : {}; const page_height_limit = page_conf.height_limit || 1000; - const article_seq = document.querySelectorAll('RT-article'); - if (article_seq.length === 0) { - RT.debug.error('pagination', 'No elements found. Pagination aborted.'); - return; - } + let measureContainer = null; - // ---------- helpers ---------- - const get_el_height = (el) => { + // ========================================================= + // 1. DOM Measurement Utilities + // ========================================================= + function getElHeight(el) { const wasInDOM = el.parentNode !== null; if (!wasInDOM) document.body.appendChild(el); const rect = el.getBoundingClientRect(); @@ -18,24 +17,21 @@ window.StyleRT.paginate_by_element = function () { const margin = parseFloat(style.marginTop) + parseFloat(style.marginBottom); if (!wasInDOM) el.remove(); return (rect.height || 0) + (margin || 0); - }; + } - // Create a hidden measurement container that mimics the article's layout - let measureContainer = null; - const getMeasureContainer = () => { + function getMeasureContainer() { if (measureContainer && measureContainer.parentNode) return measureContainer; const article = document.querySelector('RT-article'); if (!article) { const temp = document.createElement('div'); temp.style.visibility = 'hidden'; temp.style.position = 'absolute'; - temp.style.width = '100%'; // fallback + temp.style.width = '100%'; document.body.appendChild(temp); measureContainer = temp; return temp; } const container = document.createElement('div'); - // Copy the computed width and font styles from the article const articleStyle = window.getComputedStyle(article); container.style.visibility = 'hidden'; container.style.position = 'absolute'; @@ -47,31 +43,66 @@ window.StyleRT.paginate_by_element = function () { document.body.appendChild(container); measureContainer = container; return container; - }; + } - // Measure a fragment by temporarily inserting it into the measurement container - const measureFragment = (frag) => { + function measureFragment(frag) { const container = getMeasureContainer(); container.appendChild(frag); - const h = get_el_height(frag); + const h = getElHeight(frag); container.removeChild(frag); return h; - }; + } - const isSplittable = (el) => { + // ========================================================= + // STEP 1: PREPARE FOOTNOTES (Strip and tag) + // ========================================================= + const article_seq = document.querySelectorAll('RT-article'); + if (article_seq.length === 0) { + debug.error('pagination', 'No elements found. Pagination aborted.'); + return; + } + + const footnote_registry = {}; + let footnote_counter = 1; + + Array.from(article_seq).forEach(article => { + // Bulletproof extraction: immune to XML/HTML case-sensitivity parsing quirks + const all_nodes = Array.from(article.querySelectorAll('*')); + const raw_footnotes = all_nodes.filter(node => node.tagName.toLowerCase() === 'rt-footnote'); + + raw_footnotes.forEach(fn => { + const id = footnote_counter++; + footnote_registry[id] = fn.innerHTML; // Save the payload + + // Trim any standard HTML whitespace immediately preceding the tag + const prev = fn.previousSibling; + if (prev && prev.nodeType === Node.TEXT_NODE) { + prev.textContent = prev.textContent.replace(/\s+$/, ''); + } + + // Replace with a zero-height marker that rides along with the text + const marker = document.createElement('rt-fn-marker'); + marker.setAttribute('data-id', id); + + if (fn.parentNode) { + fn.parentNode.replaceChild(marker, fn); + } + }); + }); + + // ========================================================= + // Splitting Logic (Clean and undisturbed) + // ========================================================= + function isSplittable(el) { const tag = el.tagName; if (tag === 'UL' || tag === 'OL') { const items = Array.from(el.children).filter(c => c.tagName === 'LI'); if (items.length === 0) return null; - // Measure item heights once (still in DOM) - const itemHeights = items.map(li => get_el_height(li)); - - // Measure empty list overhead + const itemHeights = items.map(li => getElHeight(li)); const emptyClone = el.cloneNode(false); - const overhead = get_el_height(emptyClone); + const overhead = getElHeight(emptyClone); - // Store info on the original element (and on rest fragments later) el._splitInfo = { type: 'list', itemHeights, overhead, offset: 0 }; return makeListSplitter(el, el._splitInfo); } @@ -82,36 +113,29 @@ window.StyleRT.paginate_by_element = function () { const rows = tbody ? Array.from(tbody.rows) : Array.from(el.rows); if (rows.length === 0) return null; - const theadHeight = thead ? get_el_height(thead) : 0; - const rowHeights = rows.map(row => get_el_height(row)); + const theadHeight = thead ? getElHeight(thead) : 0; + const rowHeights = rows.map(row => getElHeight(row)); const emptyClone = el.cloneNode(false); - if (thead) { - const theadClone = thead.cloneNode(true); - emptyClone.appendChild(theadClone); - } - const tbodyClone = document.createElement('tbody'); - emptyClone.appendChild(tbodyClone); - const overhead = get_el_height(emptyClone) - theadHeight; + if (thead) emptyClone.appendChild(thead.cloneNode(true)); + emptyClone.appendChild(document.createElement('tbody')); + const overhead = getElHeight(emptyClone) - theadHeight; el._splitInfo = { type: 'table', rowHeights, overhead, theadHeight, offset: 0 }; return makeTableSplitter(el, el._splitInfo); } - - return null; // not splittable - }; + return null; + } function makeListSplitter(el, info) { return (remaining) => { const children = Array.from(el.children).filter(c => c.tagName === 'LI'); const start = info.offset; - const relevantHeights = info.itemHeights.slice(start, start + children.length); - // Build fragments iteratively and measure them for exact height let bestCount = 0; let bestHeight = 0; - // Try to include as many items as possible const tempList = el.cloneNode(false); + for (let i = 0; i < children.length; i++) { const itemClone = children[i].cloneNode(true); tempList.appendChild(itemClone); @@ -120,24 +144,18 @@ window.StyleRT.paginate_by_element = function () { bestCount = i + 1; bestHeight = fragHeight; } else { - // Remove the last item tempList.removeChild(itemClone); break; } } - if (bestCount === 0) { - return { first: null, rest: el, firstHeight: 0 }; - } + if (bestCount === 0) return { first: null, rest: el, firstHeight: 0 }; - // Build first fragment (with exactly bestCount items) const first = el.cloneNode(false); for (let i = 0; i < bestCount; i++) { first.appendChild(children[i].cloneNode(true)); } - - // Build rest fragment only if there are remaining items let rest = null; if (bestCount < children.length) { rest = el.cloneNode(false); @@ -145,13 +163,11 @@ window.StyleRT.paginate_by_element = function () { rest.appendChild(children[i].cloneNode(true)); } - // Explicitly inject the starting index for ordered lists if (el.tagName === 'OL') { const currentStart = parseInt(el.getAttribute('start'), 10) || 1; rest.setAttribute('start', currentStart + bestCount); } - // Forward split info rest._splitInfo = { type: 'list', itemHeights: info.itemHeights, @@ -164,15 +180,11 @@ window.StyleRT.paginate_by_element = function () { }; } - - function makeTableSplitter(el, info) { const thead = el.querySelector('thead'); const createShell = () => { const shell = el.cloneNode(false); - if (thead) { - shell.appendChild(thead.cloneNode(true)); - } + if (thead) shell.appendChild(thead.cloneNode(true)); const newTbody = document.createElement('tbody'); shell.appendChild(newTbody); return shell; @@ -182,41 +194,38 @@ window.StyleRT.paginate_by_element = function () { const tbody = el.querySelector('tbody'); const rows = tbody ? Array.from(tbody.rows) : Array.from(el.rows); const start = info.offset; - const relevantRows = rows.slice(start, start + rows.length); let bestCount = 0; let bestHeight = 0; const tempTable = createShell(); const tempBody = tempTable.querySelector('tbody'); - for (let i = 0; i < relevantRows.length; i++) { - tempBody.appendChild(relevantRows[i].cloneNode(true)); + + for (let i = 0; i < rows.length; i++) { + tempBody.appendChild(rows[i].cloneNode(true)); const h = measureFragment(tempTable); if (h <= remaining) { bestCount = i + 1; bestHeight = h; } else { - // Remove the last row tempBody.removeChild(tempBody.lastChild); break; } } - if (bestCount === 0) { - return { first: null, rest: el, firstHeight: 0 }; - } + if (bestCount === 0) return { first: null, rest: el, firstHeight: 0 }; const first = createShell(); const firstBody = first.querySelector('tbody'); for (let i = 0; i < bestCount; i++) { - firstBody.appendChild(relevantRows[i].cloneNode(true)); + firstBody.appendChild(rows[i].cloneNode(true)); } let rest = null; - if (bestCount < relevantRows.length) { + if (bestCount < rows.length) { rest = createShell(); const restBody = rest.querySelector('tbody'); - for (let i = bestCount; i < relevantRows.length; i++) { - restBody.appendChild(relevantRows[i].cloneNode(true)); + for (let i = bestCount; i < rows.length; i++) { + restBody.appendChild(rows[i].cloneNode(true)); } rest._splitInfo = { @@ -232,82 +241,70 @@ window.StyleRT.paginate_by_element = function () { }; } - // ---------- main pagination loop ---------- - let article_index = 0; - while (article_index < article_seq.length) { - const article = article_seq[article_index]; - + // ========================================================= + // STEP 2: NORMAL PAGINATOR + // ========================================================= + function paginateArticle(article) { const raw_element_seq = Array.from(article.children).filter(el => !['SCRIPT', 'STYLE', 'RT-PAGE'].includes(el.tagName) ); - if (raw_element_seq.length === 0) { - article_index++; - continue; - } + if (raw_element_seq.length === 0) return; const page_seq = []; let current_batch_seq = []; let current_h = 0; - let i = 0; + while (i < raw_element_seq.length) { const el = raw_element_seq[i]; const splitter = isSplittable(el); - // --- Splittable element --- if (splitter) { const remaining = page_height_limit - current_h; const { first, rest, firstHeight } = splitter(remaining); if (first) { - // Place the fitting fragment current_batch_seq.push(first); - current_h += firstHeight; // exact measured height + current_h += firstHeight; if (rest) { - // Replace original with remainder raw_element_seq.splice(i, 1, rest); } else { - // Element is completely consumed raw_element_seq.splice(i, 1); } - // Do not increment i - the next element is now at index i } else { - // Not even one item fits on this page if (current_batch_seq.length === 0) { - // Empty page -> wrap whole element in a scroll frame const frame = document.createElement('rt-scroll-frame'); frame.style.display = 'block'; frame.style.overflowY = 'auto'; frame.style.maxHeight = page_height_limit + 'px'; frame.appendChild(el); current_batch_seq.push(frame); - i++; // element consumed + i++; } else { - // Page has content -> start a new page and keep rest for later page_seq.push(current_batch_seq); current_batch_seq = []; current_h = 0; - raw_element_seq[i] = rest; + raw_element_seq[i] = rest || el; } } continue; } // --- Ordinary (non-splittable) element --- - const h = get_el_height(el); + const h = getElHeight(el); if (current_h + h > page_height_limit && current_batch_seq.length > 0) { - // Backtrack widowed headings let backtrack_seq = []; let backtrack_h = 0; + while (current_batch_seq.length > 0) { const last = current_batch_seq[current_batch_seq.length - 1]; if (!/^H[1-6]/.test(last.tagName)) break; const popped = current_batch_seq.pop(); backtrack_seq.unshift(popped); - backtrack_h += get_el_height(popped); + backtrack_h += getElHeight(popped); } if (current_batch_seq.length > 0) { @@ -341,15 +338,58 @@ window.StyleRT.paginate_by_element = function () { article.appendChild(page_el); p++; } + } - if (RT.debug) { - RT.debug.log('pagination', `Article paginated into ${page_seq.length} pages.`); - } + // Execute pagination + Array.from(article_seq).forEach(article => paginateArticle(article)); + + // ========================================================= + // STEP 3: RESOLVE FOOTNOTES & EXPAND PAGES + // ========================================================= + Array.from(article_seq).forEach(article => { + const rendered_pages = article.querySelectorAll('rt-page'); + + Array.from(rendered_pages).forEach(page => { + // Bulletproof extraction for the markers + const all_page_nodes = Array.from(page.querySelectorAll('*')); + const markers = all_page_nodes.filter(node => node.tagName.toLowerCase() === 'rt-fn-marker'); + + if (markers.length === 0) return; + + // Construct the footer block for this page + const fn_container = document.createElement('div'); + fn_container.className = 'rt-footnote-container'; + fn_container.style.borderTop = '1px solid var(--rt-border-default)'; + fn_container.style.marginTop = '2rem'; + fn_container.style.paddingTop = '1rem'; + fn_container.style.fontSize = '0.9em'; + + markers.forEach(marker => { + const id = marker.getAttribute('data-id'); + const html = footnote_registry[id]; + + // Replace the invisible marker with the visible naked superscript link + const sup = document.createElement('sup'); + sup.innerHTML = `${id}`; + + if (marker.parentNode) { + marker.parentNode.replaceChild(sup, marker); + } - article_index++; - } + // Append the actual text to the footer with a clean, print-ready number format + const fn_line = document.createElement('div'); + fn_line.id = `fn-${id}`; + fn_line.style.marginBottom = '0.5rem'; + fn_line.innerHTML = `${id}.${html}`; + fn_container.appendChild(fn_line); + }); + + // Attach the footer. The page organically stretches to fit. + page.appendChild(fn_container); + }); + }); - // Clean up measurement container + // Cleanup if (measureContainer && measureContainer.parentNode) { measureContainer.remove(); measureContainer = null; diff --git a/document/style_manual.html b/document/RT-semantic-HTML-tags.html similarity index 94% rename from document/style_manual.html rename to document/RT-semantic-HTML-tags.html index ebaf512..defa81a 100644 --- a/document/style_manual.html +++ b/document/RT-semantic-HTML-tags.html @@ -2,7 +2,7 @@ - The RT semantic HTML tags + RT semantic HTML tags