now breaks certain elements over page boundaries, fixes scroll on refresh
authorThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Thu, 14 May 2026 16:53:05 +0000 (16:53 +0000)
committerThomas Walker Lynch <eknp9n@reasoningtechnology.com>
Thu, 14 May 2026 16:53:05 +0000 (16:53 +0000)
0pus_Harmony [deleted file]
0pus_RT-style-JS_public [new file with mode: 0644]
consumer/release/RT/element/TOC.js
consumer/release/RT/layout/article_tech_ref.js
consumer/release/RT/layout/paginate_by_element.js
developer/authored/RT/element/TOC.js
developer/authored/RT/layout/article_tech_ref.js
developer/authored/RT/layout/paginate_by_element.js

diff --git a/0pus_Harmony b/0pus_Harmony
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/0pus_RT-style-JS_public b/0pus_RT-style-JS_public
new file mode 100644 (file)
index 0000000..e69de29
index eb246f5..fb6b10a 100644 (file)
   Default (No attribute):
     Context Aware. Looks backwards for the nearest heading H(N).
     Targets H(N+1). Stops at the next H(N).
+
+First heading 1               1
+    First heading 2          2
+    Next heading 2           2
+Next heading 2                3
+
 */
+
 window.StyleRT = window.StyleRT || {};
 
 window.StyleRT.TOC = function(){
@@ -20,83 +27,136 @@ window.StyleRT.TOC = function(){
   TOC_seq.forEach( (container ,TOC_index) => {
     container.style.display = 'block';
 
-    // 1. Determine Target Level
-    const attr_level = parseInt( container.getAttribute('level') );
-    let target_level;
-
-    if( !isNaN(attr_level) ){
-       // EXPLICIT MODE
-       target_level = attr_level;
-       if(debug.log) debug.log('TOC' ,`TOC #${TOC_index} explicit target: H${target_level}`);
-    } else {
-       // IMPLICIT / CONTEXT MODE
-       let context_level = 0; // Default 0 (Root)
-       let prev = container.previousElementSibling;
-       while(prev){
-         const match = prev.tagName.match(/^H([1-6])$/);
-         if(match){
-           context_level = parseInt( match[1] );
-           break;
-         }
-         prev = prev.previousElementSibling;
-       }
-       target_level = context_level + 1;
-       if(debug.log) debug.log('TOC' ,`TOC #${TOC_index} context implied target: H${target_level}`);
+    // 1. Parse attribute: single number N or range A-B
+    const attr_val = container.getAttribute('level');
+    let start_level, end_level;
+
+    if (attr_val) {
+      const rangeMatch = attr_val.match(/^(\d)-(\d)$/);
+      if (rangeMatch) {
+        const a = parseInt(rangeMatch[1]);
+        const b = parseInt(rangeMatch[2]);
+        if (a >= 1 && a <= 6 && b >= 1 && b <= 6 && a <= b) {
+          start_level = a;
+          end_level   = b;
+          if (debug.log) debug.log('TOC', `TOC #${TOC_index} range: H${a}-H${b}`);
+        } else {
+          if (debug.log) debug.log('TOC', `Invalid range "${attr_val}" → implicit mode`);
+        }
+      } else {
+        const single = parseInt(attr_val);
+        if (!isNaN(single) && single >= 1 && single <= 6) {
+          start_level = single;
+          end_level   = single;
+          if (debug.log) debug.log('TOC', `TOC #${TOC_index} single level: H${single}`);
+        } else {
+          if (debug.log) debug.log('TOC', `Invalid level "${attr_val}" → implicit mode`);
+        }
+      }
+    }
+
+    // 2. Implicit mode (no attribute or invalid)
+    if (start_level === undefined || end_level === undefined) {
+      let context_level = 0;
+      let prev = container.previousElementSibling;
+      while (prev) {
+        const match = prev.tagName.match(/^H([1-6])$/);
+        if (match) {
+          context_level = parseInt(match[1]);
+          break;
+        }
+        prev = prev.previousElementSibling;
+      }
+      const target_level = Math.min(context_level + 1, 6);
+      start_level = target_level;
+      end_level   = target_level;
+      if (debug.log) debug.log('TOC', `TOC #${TOC_index} implicit target: H${target_level}`);
     }
 
-    // Stop condition: Stop if we hit a heading that is a "parent" or "sibling" of the context.
-    // Mathematically: Stop if found_level < target_level.
-    const stop_threshold = target_level;
+    // 3. Collect all matching headings until a higher-level heading stops us
+    const headings = [];
+    let next_el = container.nextElementSibling;
+    while (next_el) {
+      const match = next_el.tagName.match(/^H([1-6])$/);
+      if (match) {
+        const found_level = parseInt(match[1]);
+
+        // Stop if we hit a heading that is a parent of the lowest level we collect
+        if (found_level < start_level) break;
+
+        // Collect if within the requested range
+        if (found_level >= start_level && found_level <= end_level) {
+          // Ensure it has an id
+          if (!next_el.id) {
+            next_el.id = `TOC-ref-${TOC_index}-${found_level}-${headings.length}`;
+          }
+          headings.push({ el: next_el, level: found_level });
+        }
+      }
+      next_el = next_el.nextElementSibling;
+    }
 
-    // 2. Setup Container
+    // 4. Build the container (title + list)
     container.innerHTML = '';
     const title = document.createElement('h1');
-    // Title logic: If targeting H1, the element serves as a Main TOC. Otherwise the element serves as a Section TOC.
-    title.textContent = target_level === 1 ? 'Table of Contents' : 'Section Contents';
+    title.textContent = start_level === 1 ? 'Table of Contents' : 'Section Contents';
     title.style.textAlign = 'center';
     container.appendChild(title);
 
-    const list = document.createElement('ul');
-    list.style.listStyle = 'none';
-    list.style.paddingLeft = '0';
-    container.appendChild(list);
+    if (headings.length === 0) return; // nothing to show
 
-    // 3. Scan Forward
-    let next_el = container.nextElementSibling;
-    while(next_el){
-      const match = next_el.tagName.match(/^H([1-6])$/);
-      if(match){
-        const found_level = parseInt( match[1] );
+    // Top-level list
+    const topList = document.createElement('ul');
+    topList.style.listStyle = 'none';
+    topList.style.paddingLeft = '0';
+    container.appendChild(topList);
+
+    // Stack of <ul> elements; index 0 = top-level list
+    const listStack = [topList];
+
+    for (const item of headings) {
+      // Depth relative to start_level
+      const depth = item.level - start_level;   // 0 = top-level, 1 = sub-level, etc.
 
-        // STOP Logic:
-        // If we are looking for H2s, we stop if we hit an H1 (level 1).
-        // If we are looking for H1s, we stop if we hit nothing (level 0).
-        if(found_level < target_level){
+      // Ensure we have the correct nesting depth
+      while (listStack.length - 1 > depth) {
+        // Pop until we are at the right depth
+        listStack.pop();
+      }
+
+      // If we need to go deeper, open new sub-lists inside the last <li>
+      while (listStack.length - 1 < depth) {
+        const parentList = listStack[listStack.length - 1];
+        const lastLi = parentList.lastElementChild;
+        if (lastLi) {
+          const subList = document.createElement('ul');
+          subList.style.listStyle = 'none';
+          subList.style.paddingLeft = '1.5rem';   // indentation for nested items
+          lastLi.appendChild(subList);
+          listStack.push(subList);
+        } else {
+          // No parent <li> yet – stay at current depth (flatten)
           break;
         }
+      }
 
-        // COLLECT Logic:
-        if(found_level === target_level){
-          if(!next_el.id) next_el.id = `TOC-ref-${TOC_index}-${found_level}-${list.children.length}`;
-
-          const li = document.createElement('li');
-          li.style.marginBottom = '0.5rem';
+      // Create the <li> for this heading
+      const li = document.createElement('li');
+      li.style.marginBottom = '0.5rem';
 
-          const a = document.createElement('a');
-          a.href = `#${next_el.id}`;
-          a.textContent = next_el.textContent;
-          a.style.textDecoration = 'none';
-          a.style.color = 'inherit';
-          a.style.display = 'block';
+      const a = document.createElement('a');
+      a.href = `#${item.el.id}`;
+      a.textContent = item.el.textContent;
+      a.style.textDecoration = 'none';
+      a.style.color = 'inherit';
+      a.style.display = 'block';
 
-          a.onmouseover = () => a.style.color = 'var(--rt-brand-primary)';
-          a.onmouseout = () => a.style.color = 'inherit';
+      a.onmouseover = () => a.style.color = 'var(--rt-brand-primary)';
+      a.onmouseout  = () => a.style.color = 'inherit';
 
-          li.appendChild(a);
-          list.appendChild(li);
-        }
-      }
-      next_el = next_el.nextElementSibling;
+      li.appendChild(a);
+      // Add to the current deepest list
+      listStack[listStack.length - 1].appendChild(li);
     }
   });
 };
index 8280bae..e9d8a80 100644 (file)
@@ -1,7 +1,7 @@
 (function(){
   const RT = window.StyleRT = window.StyleRT || {};
 
-  // 1. Declare Dependencies with updated core/ and layout/ paths
+  // 1. Declare Dependencies
   RT.include('RT/core/utility');
   RT.include('RT/element/math');
   RT.include('RT/element/code');
@@ -13,7 +13,7 @@
   RT.include('RT/layout/page_fixed_glow');
   RT.include('RT/core/body_visibility_visible');
 
-  // 2. The Typography Layout (Adapted from the original)
+  // 2. The Typography Layout
   RT.article = function(){
     RT.config = RT.config || {};
     RT.config.article = {
       style.color = "var(--rt-content-main)";
     }
 
-    // CSS injection for headers, lists, and layout mapped from theme variables
-    // 1. Adjust paginator capacity to fit the physical page minus padding
     window.StyleRT = window.StyleRT || {};
     window.StyleRT.config = window.StyleRT.config || {};
     window.StyleRT.config.page = window.StyleRT.config.page || {};
-    // 1056px - 96px (padding) = 960px usable. 920px leaves room for the footer.
-    window.StyleRT.config.page.height_limit = 920; 
+    window.StyleRT.config.page.height_limit = 900; 
 
-    // 2. CSS injection for headers, lists, and layout mapped from theme variables
     const style_node = document.createElement("style");
     style_node.innerHTML = `
       rt-article {
         font-family: 'Noto Sans JP', Arial, sans-serif;
         background-color: var(--rt-surface-0);
         color: var(--rt-content-main);
-        /* Force width so inline JS doesn't widen the measurement area */
         max-width: 46.875rem !important; 
         box-sizing: border-box !important;
       }
       
-      /* PRE-PAGINATION: Mimic page padding so text line-wraps accurately during measurement */
       rt-article:not(:has(rt-page)) {
         padding: 3rem !important; 
       }
       
-      /* POST-PAGINATION: Remove article padding so we don't double-pad the generated pages */
       rt-article:has(rt-page) {
         padding: 0 !important;
       }
 
       rt-article rt-page {
+        position: relative;
         display: block;
         padding: 3rem;
         margin: 1.25rem auto;
@@ -85,7 +79,6 @@
         box-shadow: 0 0 0.625rem var(--rt-brand-primary);
       }
       
-      /* --- HEADER CASCADE --- */
       rt-article h1 {
         font-size: 1.5rem;
         text-align: center;
         margin-left: 8ch;
       }
 
-      /* --- BODY TEXT (Flush Left) --- */
       rt-article p,
       rt-article ul,
       rt-article ol {
         margin-bottom: 0.5rem;
       }
       
-      /* --- CODE FORMATTING --- */
       rt-article rt-code {
         font-family: 'Courier New', Courier, monospace;
         background-color: var(--rt-surface-code);
 
   // 3. The Execution Sequence
   const run_semantics = function(){
+    // Override browser scroll memory
+    if ('scrollRestoration' in history) {
+      history.scrollRestoration = 'manual';
+    }
+
+    // Capture the scroll position right before the page unloads
+    window.addEventListener('beforeunload', () => {
+      sessionStorage.setItem('RT_scroll_position', window.scrollY);
+    });
+
     if(RT.theme) RT.theme();     
     RT.article(); 
     
-    // Call the newly renamed element functions
     if(RT.title) RT.title(); 
     if(RT.term) RT.term();
     if(RT.math) RT.math();
     if(RT.code) RT.code();
 
-    // Check for MathJax typesetting
     if(window.MathJax && MathJax.Hub && MathJax.Hub.Queue){
       MathJax.Hub.Queue( ["Typeset" ,MathJax.Hub] ,run_layout );
     }else{
     if(RT.paginate_by_element) RT.paginate_by_element();
     if(RT.page) RT.page();
     if(RT.body_visibility_visible) RT.body_visibility_visible();
+    
+    // Yield to the browser's rendering pipeline.
+    // The DOM has been mutated, but the browser needs a moment to recalculate
+    // the physical geometry before we apply a scroll coordinate.
+    setTimeout(() => {
+      if (window.location.hash) {
+        const target = document.getElementById(window.location.hash.substring(1));
+        if (target) {
+          target.scrollIntoView();
+        }
+      } else {
+        const saved_y = sessionStorage.getItem('RT_scroll_position');
+        if (saved_y !== null) {
+          window.scrollTo(0, parseInt(saved_y, 10));
+        } else {
+          window.scrollTo(0, 0);
+        }
+      }
+    }, 50); 
   };
 
   // 4. Bind to DOM Ready
-  // This replaces the need to put a script at the bottom of the HTML body
   document.addEventListener( 'DOMContentLoaded' ,run_semantics );
 
 })();
index 89595ca..9c5ba11 100644 (file)
-/*
-  Layout Paginator: paginate_by_element
-*/
-window.StyleRT = window.StyleRT || {};
-
-window.StyleRT.paginate_by_element = function(){
+window.StyleRT.paginate_by_element = function () {
   const RT = window.StyleRT;
   const page_conf = (RT.config && RT.config.page) ? RT.config.page : {};
-  const page_height_limit = page_conf.height_limit || 1000; 
+  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 <RT-article> elements found. Pagination aborted.');
+  const article_seq = document.querySelectorAll('RT-article');
+  if (article_seq.length === 0) {
+    RT.debug.error('pagination''No <RT-article> elements found. Pagination aborted.');
     return;
   }
 
+  // ---------- helpers ----------
+  const get_el_height = (el) => {
+    const wasInDOM = el.parentNode !== null;
+    if (!wasInDOM) document.body.appendChild(el);
+    const rect = el.getBoundingClientRect();
+    const style = window.getComputedStyle(el);
+    const margin = parseFloat(style.marginTop) + parseFloat(style.marginBottom);
+    if (!wasInDOM) el.remove();
+    return (rect.height || 0) + (margin || 0);
+  };
+
+  let measureContainer = null;
+  const 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%';
+      document.body.appendChild(temp);
+      measureContainer = temp;
+      return temp;
+    }
+    const container = document.createElement('div');
+    const articleStyle = window.getComputedStyle(article);
+    container.style.visibility = 'hidden';
+    container.style.position = 'absolute';
+    container.style.width = articleStyle.width;
+    container.style.fontFamily = articleStyle.fontFamily;
+    container.style.fontSize = articleStyle.fontSize;
+    container.style.lineHeight = articleStyle.lineHeight;
+    container.style.fontWeight = articleStyle.fontWeight;
+    document.body.appendChild(container);
+    measureContainer = container;
+    return container;
+  };
+
+  const measureFragment = (frag) => {
+    const container = getMeasureContainer();
+    container.appendChild(frag);
+    const h = get_el_height(frag);
+    container.removeChild(frag);
+    return h;
+  };
+
+  const isSplittable = (el) => {
+    const tag = el.tagName;
+
+    // Support for custom RT-TOC wrapper
+    if (tag === 'RT-TOC') {
+      const list = el.querySelector('ul, ol');
+      if (!list) return null;
+
+      const items = Array.from(list.children).filter(c => c.tagName === 'LI');
+      if (items.length === 0) return null;
+
+      const itemHeights = items.map(li => get_el_height(li));
+
+      const emptyClone = el.cloneNode(true);
+      const listInClone = emptyClone.querySelector('ul, ol');
+      if (listInClone) listInClone.innerHTML = '';
+      const overhead = get_el_height(emptyClone);
+
+      el._splitInfo = { type: 'toc', itemHeights, overhead, offset: 0 };
+      return makeTOCSplitter(el, el._splitInfo);
+    }
+
+    if (tag === 'UL' || tag === 'OL') {
+      const items = Array.from(el.children).filter(c => c.tagName === 'LI');
+      if (items.length === 0) return null;
+
+      const itemHeights = items.map(li => get_el_height(li));
+
+      const emptyClone = el.cloneNode(false);
+      const overhead = get_el_height(emptyClone);
+
+      el._splitInfo = { type: 'list', itemHeights, overhead, offset: 0 };
+      return makeListSplitter(el, el._splitInfo);
+    }
+
+    if (tag === 'TABLE') {
+      const thead = el.querySelector('thead');
+      const tbody = el.querySelector('tbody');
+      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 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;
+
+      el._splitInfo = { type: 'table', rowHeights, overhead, theadHeight, offset: 0 };
+      return makeTableSplitter(el, el._splitInfo);
+    }
+
+    return null;
+  };
+
+  function makeTOCSplitter(el, info) {
+    return (remaining) => {
+      const list = el.querySelector('ul, ol');
+      const children = Array.from(list.children).filter(c => c.tagName === 'LI');
+      const start = info.offset;
+
+      let bestCount = 0;
+      let bestHeight = 0;
+
+      const tempClone = el.cloneNode(true);
+      const tempList = tempClone.querySelector('ul, ol');
+      tempList.innerHTML = '';
+
+      for (let i = 0; i < children.length; i++) {
+        const itemClone = children[i].cloneNode(true);
+        tempList.appendChild(itemClone);
+        const fragHeight = measureFragment(tempClone);
+        if (fragHeight <= remaining) {
+          bestCount = i + 1;
+          bestHeight = fragHeight;
+        } else {
+          tempList.removeChild(itemClone);
+          break;
+        }
+      }
+
+      if (bestCount === 0) {
+        return { first: null, rest: el, firstHeight: 0 };
+      }
+
+      const first = el.cloneNode(true);
+      const firstList = first.querySelector('ul, ol');
+      firstList.innerHTML = '';
+      for (let i = 0; i < bestCount; i++) {
+        firstList.appendChild(children[i].cloneNode(true));
+      }
+
+      const rest = el.cloneNode(true);
+      const restList = rest.querySelector('ul, ol');
+      restList.innerHTML = '';
+      for (let i = bestCount; i < children.length; i++) {
+        restList.appendChild(children[i].cloneNode(true));
+      }
+
+      // Clean up the title heading in the continuation fragment
+      const title = rest.querySelector('h1, h2, h3, h4, h5, h6');
+      if (title) {
+        title.remove();
+      }
+
+      // Recalculate overhead since the title is now gone
+      const emptyRest = rest.cloneNode(true);
+      const emptyRestList = emptyRest.querySelector('ul, ol');
+      if (emptyRestList) {
+        emptyRestList.innerHTML = '';
+      }
+      const restOverhead = measureFragment(emptyRest);
+
+      rest._splitInfo = {
+        type: 'toc',
+        itemHeights: info.itemHeights,
+        overhead: restOverhead,
+        offset: start + bestCount
+      };
+
+      return { first, rest, firstHeight: bestHeight };
+    };
+  }
+
+  function makeListSplitter(el, info) {
+    return (remaining) => {
+      const children = Array.from(el.children).filter(c => c.tagName === 'LI');
+      const start = info.offset;
+
+      let bestCount = 0;
+      let bestHeight = 0;
+      const tempList = el.cloneNode(false);
+      for (let i = 0; i < children.length; i++) {
+        const itemClone = children[i].cloneNode(true);
+        tempList.appendChild(itemClone);
+        const fragHeight = measureFragment(tempList);
+        if (fragHeight <= remaining) {
+          bestCount = i + 1;
+          bestHeight = fragHeight;
+        } else {
+          tempList.removeChild(itemClone);
+          break;
+        }
+      }
+
+      if (bestCount === 0) {
+        return { first: null, rest: el, firstHeight: 0 };
+      }
+
+      const first = el.cloneNode(false);
+      for (let i = 0; i < bestCount; i++) {
+        first.appendChild(children[i].cloneNode(true));
+      }
+
+      const rest = el.cloneNode(false);
+      for (let i = bestCount; i < children.length; i++) {
+        rest.appendChild(children[i].cloneNode(true));
+      }
+
+      rest._splitInfo = {
+        type: 'list',
+        itemHeights: info.itemHeights,
+        overhead: info.overhead,
+        offset: start + bestCount
+      };
+
+      return { first, rest, firstHeight: bestHeight };
+    };
+  }
+
+  function makeTableSplitter(el, info) {
+    const thead = el.querySelector('thead');
+    const createShell = () => {
+      const shell = el.cloneNode(false);
+      if (thead) {
+        shell.appendChild(thead.cloneNode(true));
+      }
+      const newTbody = document.createElement('tbody');
+      shell.appendChild(newTbody);
+      return shell;
+    };
+
+    return (remaining) => {
+      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));
+        const h = measureFragment(tempTable);
+        if (h <= remaining) {
+          bestCount = i + 1;
+          bestHeight = h;
+        } else {
+          tempBody.removeChild(tempBody.lastChild);
+          break;
+        }
+      }
+
+      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));
+      }
+
+      const rest = createShell();
+      const restBody = rest.querySelector('tbody');
+      for (let i = bestCount; i < relevantRows.length; i++) {
+        restBody.appendChild(relevantRows[i].cloneNode(true));
+      }
+
+      rest._splitInfo = {
+        type: 'table',
+        rowHeights: info.rowHeights,
+        overhead: info.overhead,
+        theadHeight: info.theadHeight,
+        offset: start + bestCount
+      };
+
+      return { first, rest, firstHeight: bestHeight };
+    };
+  }
+
+  // ---------- main pagination loop ----------
   let article_index = 0;
-  while(true){
-    if(article_index === article_seq.length) break;
+  while (article_index < article_seq.length) {
     const article = article_seq[article_index];
-    
-    const raw_element_seq = Array.from(article.children).filter( el => 
-      !['SCRIPT' ,'STYLE' ,'RT-PAGE'].includes(el.tagName)
+
+    const raw_element_seq = Array.from(article.children).filter(el =>
+      !['SCRIPT', 'STYLE', 'RT-PAGE'].includes(el.tagName)
     );
 
-    if(raw_element_seq.length > 0){
-      const page_seq = [];
-      let current_batch_seq = [];
-      let current_h = 0;
+    if (raw_element_seq.length === 0) {
+      article_index++;
+      continue;
+    }
 
-      const get_el_height = (el) => {
-        const rect = el.getBoundingClientRect();
-        const style = window.getComputedStyle(el);
-        const margin = parseFloat(style.marginTop) + parseFloat(style.marginBottom);
-        return (rect.height || 0) + (margin || 0);
-      };
+    const page_seq = [];
+    let current_batch_seq = [];
+    let current_h = 0;
 
-      let i = 0;
-      while(true){
-        if(i === raw_element_seq.length) break;
-        const el = raw_element_seq[i];
-        const h = get_el_height(el);
-
-        if(current_h + h > page_height_limit && current_batch_seq.length > 0){
-          let backtrack_seq = [];
-          let backtrack_h = 0;
-
-          // Backtrack to rescue any widowed headings at the end of the batch
-          while(true){
-            if(current_batch_seq.length === 0) break;
-            const last_el = current_batch_seq[current_batch_seq.length - 1];
-            if(!/^H[1-6]/.test(last_el.tagName)) break;
-            
-            const popped_el = current_batch_seq.pop();
-            backtrack_seq.unshift(popped_el);
-            backtrack_h += get_el_height(popped_el);
-          }
+    let i = 0;
+    while (i < raw_element_seq.length) {
+      const el = raw_element_seq[i];
+      const splitter = isSplittable(el);
+
+      if (splitter) {
+        const remaining = page_height_limit - current_h;
+        const { first, rest, firstHeight } = splitter(remaining);
+
+        if (first) {
+          current_batch_seq.push(first);
+          current_h += firstHeight;
 
-          if(current_batch_seq.length > 0){
+          raw_element_seq.splice(i, 1, rest);
+        } else {
+          if (current_batch_seq.length === 0) {
+            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++;
+          } else {
             page_seq.push(current_batch_seq);
-            current_batch_seq = backtrack_seq;
-            current_h = backtrack_h;
-          }else{
-            // Fallback for an extreme case where the entire page was a cascade of headings
-            page_seq.push(backtrack_seq);
             current_batch_seq = [];
             current_h = 0;
+            raw_element_seq[i] = rest;
           }
         }
-
-        current_batch_seq.push(el);
-        current_h += h;
-
-        i++;
+        continue;
       }
 
-      if(current_batch_seq.length > 0){
-        page_seq.push(current_batch_seq);
-      }
+      const h = get_el_height(el);
 
-      article.innerHTML = '';
-      
-      let p = 0;
-      while(true){
-        if(p === page_seq.length) break;
-        const batch_seq = page_seq[p];
-        const page_el = document.createElement('rt-page');
-        page_el.id = `page-${p + 1}`;
-        
-        let item_idx = 0;
-        while(true){
-          if(item_idx === batch_seq.length) break;
-          page_el.appendChild(batch_seq[item_idx]);
-          item_idx++;
+      if (current_h + h > page_height_limit && current_batch_seq.length > 0) {
+        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);
         }
-        
-        article.appendChild(page_el);
-        p++;
-      }
 
-      if(RT.debug){
-        RT.debug.log('pagination' ,`Article paginated into ${page_seq.length} pages.`);
+        if (current_batch_seq.length > 0) {
+          page_seq.push(current_batch_seq);
+          current_batch_seq = backtrack_seq;
+          current_h = backtrack_h;
+        } else {
+          page_seq.push(backtrack_seq);
+          current_batch_seq = [];
+          current_h = 0;
+        }
       }
+
+      current_batch_seq.push(el);
+      current_h += h;
+      i++;
+    }
+
+    if (current_batch_seq.length > 0) {
+      page_seq.push(current_batch_seq);
+    }
+
+    article.innerHTML = '';
+    let p = 0;
+    while (p < page_seq.length) {
+      const batch = page_seq[p];
+      const page_el = document.createElement('rt-page');
+      page_el.id = `page-${p + 1}`;
+      batch.forEach(item => page_el.appendChild(item));
+      article.appendChild(page_el);
+      p++;
+    }
+
+    if (RT.debug) {
+      RT.debug.log('pagination', `Article paginated into ${page_seq.length} pages.`);
     }
 
     article_index++;
   }
+
+  if (measureContainer && measureContainer.parentNode) {
+    measureContainer.remove();
+    measureContainer = null;
+  }
 };
index eb246f5..fb6b10a 100644 (file)
   Default (No attribute):
     Context Aware. Looks backwards for the nearest heading H(N).
     Targets H(N+1). Stops at the next H(N).
+
+First heading 1               1
+    First heading 2          2
+    Next heading 2           2
+Next heading 2                3
+
 */
+
 window.StyleRT = window.StyleRT || {};
 
 window.StyleRT.TOC = function(){
@@ -20,83 +27,136 @@ window.StyleRT.TOC = function(){
   TOC_seq.forEach( (container ,TOC_index) => {
     container.style.display = 'block';
 
-    // 1. Determine Target Level
-    const attr_level = parseInt( container.getAttribute('level') );
-    let target_level;
-
-    if( !isNaN(attr_level) ){
-       // EXPLICIT MODE
-       target_level = attr_level;
-       if(debug.log) debug.log('TOC' ,`TOC #${TOC_index} explicit target: H${target_level}`);
-    } else {
-       // IMPLICIT / CONTEXT MODE
-       let context_level = 0; // Default 0 (Root)
-       let prev = container.previousElementSibling;
-       while(prev){
-         const match = prev.tagName.match(/^H([1-6])$/);
-         if(match){
-           context_level = parseInt( match[1] );
-           break;
-         }
-         prev = prev.previousElementSibling;
-       }
-       target_level = context_level + 1;
-       if(debug.log) debug.log('TOC' ,`TOC #${TOC_index} context implied target: H${target_level}`);
+    // 1. Parse attribute: single number N or range A-B
+    const attr_val = container.getAttribute('level');
+    let start_level, end_level;
+
+    if (attr_val) {
+      const rangeMatch = attr_val.match(/^(\d)-(\d)$/);
+      if (rangeMatch) {
+        const a = parseInt(rangeMatch[1]);
+        const b = parseInt(rangeMatch[2]);
+        if (a >= 1 && a <= 6 && b >= 1 && b <= 6 && a <= b) {
+          start_level = a;
+          end_level   = b;
+          if (debug.log) debug.log('TOC', `TOC #${TOC_index} range: H${a}-H${b}`);
+        } else {
+          if (debug.log) debug.log('TOC', `Invalid range "${attr_val}" → implicit mode`);
+        }
+      } else {
+        const single = parseInt(attr_val);
+        if (!isNaN(single) && single >= 1 && single <= 6) {
+          start_level = single;
+          end_level   = single;
+          if (debug.log) debug.log('TOC', `TOC #${TOC_index} single level: H${single}`);
+        } else {
+          if (debug.log) debug.log('TOC', `Invalid level "${attr_val}" → implicit mode`);
+        }
+      }
+    }
+
+    // 2. Implicit mode (no attribute or invalid)
+    if (start_level === undefined || end_level === undefined) {
+      let context_level = 0;
+      let prev = container.previousElementSibling;
+      while (prev) {
+        const match = prev.tagName.match(/^H([1-6])$/);
+        if (match) {
+          context_level = parseInt(match[1]);
+          break;
+        }
+        prev = prev.previousElementSibling;
+      }
+      const target_level = Math.min(context_level + 1, 6);
+      start_level = target_level;
+      end_level   = target_level;
+      if (debug.log) debug.log('TOC', `TOC #${TOC_index} implicit target: H${target_level}`);
     }
 
-    // Stop condition: Stop if we hit a heading that is a "parent" or "sibling" of the context.
-    // Mathematically: Stop if found_level < target_level.
-    const stop_threshold = target_level;
+    // 3. Collect all matching headings until a higher-level heading stops us
+    const headings = [];
+    let next_el = container.nextElementSibling;
+    while (next_el) {
+      const match = next_el.tagName.match(/^H([1-6])$/);
+      if (match) {
+        const found_level = parseInt(match[1]);
+
+        // Stop if we hit a heading that is a parent of the lowest level we collect
+        if (found_level < start_level) break;
+
+        // Collect if within the requested range
+        if (found_level >= start_level && found_level <= end_level) {
+          // Ensure it has an id
+          if (!next_el.id) {
+            next_el.id = `TOC-ref-${TOC_index}-${found_level}-${headings.length}`;
+          }
+          headings.push({ el: next_el, level: found_level });
+        }
+      }
+      next_el = next_el.nextElementSibling;
+    }
 
-    // 2. Setup Container
+    // 4. Build the container (title + list)
     container.innerHTML = '';
     const title = document.createElement('h1');
-    // Title logic: If targeting H1, the element serves as a Main TOC. Otherwise the element serves as a Section TOC.
-    title.textContent = target_level === 1 ? 'Table of Contents' : 'Section Contents';
+    title.textContent = start_level === 1 ? 'Table of Contents' : 'Section Contents';
     title.style.textAlign = 'center';
     container.appendChild(title);
 
-    const list = document.createElement('ul');
-    list.style.listStyle = 'none';
-    list.style.paddingLeft = '0';
-    container.appendChild(list);
+    if (headings.length === 0) return; // nothing to show
 
-    // 3. Scan Forward
-    let next_el = container.nextElementSibling;
-    while(next_el){
-      const match = next_el.tagName.match(/^H([1-6])$/);
-      if(match){
-        const found_level = parseInt( match[1] );
+    // Top-level list
+    const topList = document.createElement('ul');
+    topList.style.listStyle = 'none';
+    topList.style.paddingLeft = '0';
+    container.appendChild(topList);
+
+    // Stack of <ul> elements; index 0 = top-level list
+    const listStack = [topList];
+
+    for (const item of headings) {
+      // Depth relative to start_level
+      const depth = item.level - start_level;   // 0 = top-level, 1 = sub-level, etc.
 
-        // STOP Logic:
-        // If we are looking for H2s, we stop if we hit an H1 (level 1).
-        // If we are looking for H1s, we stop if we hit nothing (level 0).
-        if(found_level < target_level){
+      // Ensure we have the correct nesting depth
+      while (listStack.length - 1 > depth) {
+        // Pop until we are at the right depth
+        listStack.pop();
+      }
+
+      // If we need to go deeper, open new sub-lists inside the last <li>
+      while (listStack.length - 1 < depth) {
+        const parentList = listStack[listStack.length - 1];
+        const lastLi = parentList.lastElementChild;
+        if (lastLi) {
+          const subList = document.createElement('ul');
+          subList.style.listStyle = 'none';
+          subList.style.paddingLeft = '1.5rem';   // indentation for nested items
+          lastLi.appendChild(subList);
+          listStack.push(subList);
+        } else {
+          // No parent <li> yet – stay at current depth (flatten)
           break;
         }
+      }
 
-        // COLLECT Logic:
-        if(found_level === target_level){
-          if(!next_el.id) next_el.id = `TOC-ref-${TOC_index}-${found_level}-${list.children.length}`;
-
-          const li = document.createElement('li');
-          li.style.marginBottom = '0.5rem';
+      // Create the <li> for this heading
+      const li = document.createElement('li');
+      li.style.marginBottom = '0.5rem';
 
-          const a = document.createElement('a');
-          a.href = `#${next_el.id}`;
-          a.textContent = next_el.textContent;
-          a.style.textDecoration = 'none';
-          a.style.color = 'inherit';
-          a.style.display = 'block';
+      const a = document.createElement('a');
+      a.href = `#${item.el.id}`;
+      a.textContent = item.el.textContent;
+      a.style.textDecoration = 'none';
+      a.style.color = 'inherit';
+      a.style.display = 'block';
 
-          a.onmouseover = () => a.style.color = 'var(--rt-brand-primary)';
-          a.onmouseout = () => a.style.color = 'inherit';
+      a.onmouseover = () => a.style.color = 'var(--rt-brand-primary)';
+      a.onmouseout  = () => a.style.color = 'inherit';
 
-          li.appendChild(a);
-          list.appendChild(li);
-        }
-      }
-      next_el = next_el.nextElementSibling;
+      li.appendChild(a);
+      // Add to the current deepest list
+      listStack[listStack.length - 1].appendChild(li);
     }
   });
 };
index 8280bae..e9d8a80 100644 (file)
@@ -1,7 +1,7 @@
 (function(){
   const RT = window.StyleRT = window.StyleRT || {};
 
-  // 1. Declare Dependencies with updated core/ and layout/ paths
+  // 1. Declare Dependencies
   RT.include('RT/core/utility');
   RT.include('RT/element/math');
   RT.include('RT/element/code');
@@ -13,7 +13,7 @@
   RT.include('RT/layout/page_fixed_glow');
   RT.include('RT/core/body_visibility_visible');
 
-  // 2. The Typography Layout (Adapted from the original)
+  // 2. The Typography Layout
   RT.article = function(){
     RT.config = RT.config || {};
     RT.config.article = {
       style.color = "var(--rt-content-main)";
     }
 
-    // CSS injection for headers, lists, and layout mapped from theme variables
-    // 1. Adjust paginator capacity to fit the physical page minus padding
     window.StyleRT = window.StyleRT || {};
     window.StyleRT.config = window.StyleRT.config || {};
     window.StyleRT.config.page = window.StyleRT.config.page || {};
-    // 1056px - 96px (padding) = 960px usable. 920px leaves room for the footer.
-    window.StyleRT.config.page.height_limit = 920; 
+    window.StyleRT.config.page.height_limit = 900; 
 
-    // 2. CSS injection for headers, lists, and layout mapped from theme variables
     const style_node = document.createElement("style");
     style_node.innerHTML = `
       rt-article {
         font-family: 'Noto Sans JP', Arial, sans-serif;
         background-color: var(--rt-surface-0);
         color: var(--rt-content-main);
-        /* Force width so inline JS doesn't widen the measurement area */
         max-width: 46.875rem !important; 
         box-sizing: border-box !important;
       }
       
-      /* PRE-PAGINATION: Mimic page padding so text line-wraps accurately during measurement */
       rt-article:not(:has(rt-page)) {
         padding: 3rem !important; 
       }
       
-      /* POST-PAGINATION: Remove article padding so we don't double-pad the generated pages */
       rt-article:has(rt-page) {
         padding: 0 !important;
       }
 
       rt-article rt-page {
+        position: relative;
         display: block;
         padding: 3rem;
         margin: 1.25rem auto;
@@ -85,7 +79,6 @@
         box-shadow: 0 0 0.625rem var(--rt-brand-primary);
       }
       
-      /* --- HEADER CASCADE --- */
       rt-article h1 {
         font-size: 1.5rem;
         text-align: center;
         margin-left: 8ch;
       }
 
-      /* --- BODY TEXT (Flush Left) --- */
       rt-article p,
       rt-article ul,
       rt-article ol {
         margin-bottom: 0.5rem;
       }
       
-      /* --- CODE FORMATTING --- */
       rt-article rt-code {
         font-family: 'Courier New', Courier, monospace;
         background-color: var(--rt-surface-code);
 
   // 3. The Execution Sequence
   const run_semantics = function(){
+    // Override browser scroll memory
+    if ('scrollRestoration' in history) {
+      history.scrollRestoration = 'manual';
+    }
+
+    // Capture the scroll position right before the page unloads
+    window.addEventListener('beforeunload', () => {
+      sessionStorage.setItem('RT_scroll_position', window.scrollY);
+    });
+
     if(RT.theme) RT.theme();     
     RT.article(); 
     
-    // Call the newly renamed element functions
     if(RT.title) RT.title(); 
     if(RT.term) RT.term();
     if(RT.math) RT.math();
     if(RT.code) RT.code();
 
-    // Check for MathJax typesetting
     if(window.MathJax && MathJax.Hub && MathJax.Hub.Queue){
       MathJax.Hub.Queue( ["Typeset" ,MathJax.Hub] ,run_layout );
     }else{
     if(RT.paginate_by_element) RT.paginate_by_element();
     if(RT.page) RT.page();
     if(RT.body_visibility_visible) RT.body_visibility_visible();
+    
+    // Yield to the browser's rendering pipeline.
+    // The DOM has been mutated, but the browser needs a moment to recalculate
+    // the physical geometry before we apply a scroll coordinate.
+    setTimeout(() => {
+      if (window.location.hash) {
+        const target = document.getElementById(window.location.hash.substring(1));
+        if (target) {
+          target.scrollIntoView();
+        }
+      } else {
+        const saved_y = sessionStorage.getItem('RT_scroll_position');
+        if (saved_y !== null) {
+          window.scrollTo(0, parseInt(saved_y, 10));
+        } else {
+          window.scrollTo(0, 0);
+        }
+      }
+    }, 50); 
   };
 
   // 4. Bind to DOM Ready
-  // This replaces the need to put a script at the bottom of the HTML body
   document.addEventListener( 'DOMContentLoaded' ,run_semantics );
 
 })();
index 89595ca..9c5ba11 100644 (file)
-/*
-  Layout Paginator: paginate_by_element
-*/
-window.StyleRT = window.StyleRT || {};
-
-window.StyleRT.paginate_by_element = function(){
+window.StyleRT.paginate_by_element = function () {
   const RT = window.StyleRT;
   const page_conf = (RT.config && RT.config.page) ? RT.config.page : {};
-  const page_height_limit = page_conf.height_limit || 1000; 
+  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 <RT-article> elements found. Pagination aborted.');
+  const article_seq = document.querySelectorAll('RT-article');
+  if (article_seq.length === 0) {
+    RT.debug.error('pagination''No <RT-article> elements found. Pagination aborted.');
     return;
   }
 
+  // ---------- helpers ----------
+  const get_el_height = (el) => {
+    const wasInDOM = el.parentNode !== null;
+    if (!wasInDOM) document.body.appendChild(el);
+    const rect = el.getBoundingClientRect();
+    const style = window.getComputedStyle(el);
+    const margin = parseFloat(style.marginTop) + parseFloat(style.marginBottom);
+    if (!wasInDOM) el.remove();
+    return (rect.height || 0) + (margin || 0);
+  };
+
+  let measureContainer = null;
+  const 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%';
+      document.body.appendChild(temp);
+      measureContainer = temp;
+      return temp;
+    }
+    const container = document.createElement('div');
+    const articleStyle = window.getComputedStyle(article);
+    container.style.visibility = 'hidden';
+    container.style.position = 'absolute';
+    container.style.width = articleStyle.width;
+    container.style.fontFamily = articleStyle.fontFamily;
+    container.style.fontSize = articleStyle.fontSize;
+    container.style.lineHeight = articleStyle.lineHeight;
+    container.style.fontWeight = articleStyle.fontWeight;
+    document.body.appendChild(container);
+    measureContainer = container;
+    return container;
+  };
+
+  const measureFragment = (frag) => {
+    const container = getMeasureContainer();
+    container.appendChild(frag);
+    const h = get_el_height(frag);
+    container.removeChild(frag);
+    return h;
+  };
+
+  const isSplittable = (el) => {
+    const tag = el.tagName;
+
+    // Support for custom RT-TOC wrapper
+    if (tag === 'RT-TOC') {
+      const list = el.querySelector('ul, ol');
+      if (!list) return null;
+
+      const items = Array.from(list.children).filter(c => c.tagName === 'LI');
+      if (items.length === 0) return null;
+
+      const itemHeights = items.map(li => get_el_height(li));
+
+      const emptyClone = el.cloneNode(true);
+      const listInClone = emptyClone.querySelector('ul, ol');
+      if (listInClone) listInClone.innerHTML = '';
+      const overhead = get_el_height(emptyClone);
+
+      el._splitInfo = { type: 'toc', itemHeights, overhead, offset: 0 };
+      return makeTOCSplitter(el, el._splitInfo);
+    }
+
+    if (tag === 'UL' || tag === 'OL') {
+      const items = Array.from(el.children).filter(c => c.tagName === 'LI');
+      if (items.length === 0) return null;
+
+      const itemHeights = items.map(li => get_el_height(li));
+
+      const emptyClone = el.cloneNode(false);
+      const overhead = get_el_height(emptyClone);
+
+      el._splitInfo = { type: 'list', itemHeights, overhead, offset: 0 };
+      return makeListSplitter(el, el._splitInfo);
+    }
+
+    if (tag === 'TABLE') {
+      const thead = el.querySelector('thead');
+      const tbody = el.querySelector('tbody');
+      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 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;
+
+      el._splitInfo = { type: 'table', rowHeights, overhead, theadHeight, offset: 0 };
+      return makeTableSplitter(el, el._splitInfo);
+    }
+
+    return null;
+  };
+
+  function makeTOCSplitter(el, info) {
+    return (remaining) => {
+      const list = el.querySelector('ul, ol');
+      const children = Array.from(list.children).filter(c => c.tagName === 'LI');
+      const start = info.offset;
+
+      let bestCount = 0;
+      let bestHeight = 0;
+
+      const tempClone = el.cloneNode(true);
+      const tempList = tempClone.querySelector('ul, ol');
+      tempList.innerHTML = '';
+
+      for (let i = 0; i < children.length; i++) {
+        const itemClone = children[i].cloneNode(true);
+        tempList.appendChild(itemClone);
+        const fragHeight = measureFragment(tempClone);
+        if (fragHeight <= remaining) {
+          bestCount = i + 1;
+          bestHeight = fragHeight;
+        } else {
+          tempList.removeChild(itemClone);
+          break;
+        }
+      }
+
+      if (bestCount === 0) {
+        return { first: null, rest: el, firstHeight: 0 };
+      }
+
+      const first = el.cloneNode(true);
+      const firstList = first.querySelector('ul, ol');
+      firstList.innerHTML = '';
+      for (let i = 0; i < bestCount; i++) {
+        firstList.appendChild(children[i].cloneNode(true));
+      }
+
+      const rest = el.cloneNode(true);
+      const restList = rest.querySelector('ul, ol');
+      restList.innerHTML = '';
+      for (let i = bestCount; i < children.length; i++) {
+        restList.appendChild(children[i].cloneNode(true));
+      }
+
+      // Clean up the title heading in the continuation fragment
+      const title = rest.querySelector('h1, h2, h3, h4, h5, h6');
+      if (title) {
+        title.remove();
+      }
+
+      // Recalculate overhead since the title is now gone
+      const emptyRest = rest.cloneNode(true);
+      const emptyRestList = emptyRest.querySelector('ul, ol');
+      if (emptyRestList) {
+        emptyRestList.innerHTML = '';
+      }
+      const restOverhead = measureFragment(emptyRest);
+
+      rest._splitInfo = {
+        type: 'toc',
+        itemHeights: info.itemHeights,
+        overhead: restOverhead,
+        offset: start + bestCount
+      };
+
+      return { first, rest, firstHeight: bestHeight };
+    };
+  }
+
+  function makeListSplitter(el, info) {
+    return (remaining) => {
+      const children = Array.from(el.children).filter(c => c.tagName === 'LI');
+      const start = info.offset;
+
+      let bestCount = 0;
+      let bestHeight = 0;
+      const tempList = el.cloneNode(false);
+      for (let i = 0; i < children.length; i++) {
+        const itemClone = children[i].cloneNode(true);
+        tempList.appendChild(itemClone);
+        const fragHeight = measureFragment(tempList);
+        if (fragHeight <= remaining) {
+          bestCount = i + 1;
+          bestHeight = fragHeight;
+        } else {
+          tempList.removeChild(itemClone);
+          break;
+        }
+      }
+
+      if (bestCount === 0) {
+        return { first: null, rest: el, firstHeight: 0 };
+      }
+
+      const first = el.cloneNode(false);
+      for (let i = 0; i < bestCount; i++) {
+        first.appendChild(children[i].cloneNode(true));
+      }
+
+      const rest = el.cloneNode(false);
+      for (let i = bestCount; i < children.length; i++) {
+        rest.appendChild(children[i].cloneNode(true));
+      }
+
+      rest._splitInfo = {
+        type: 'list',
+        itemHeights: info.itemHeights,
+        overhead: info.overhead,
+        offset: start + bestCount
+      };
+
+      return { first, rest, firstHeight: bestHeight };
+    };
+  }
+
+  function makeTableSplitter(el, info) {
+    const thead = el.querySelector('thead');
+    const createShell = () => {
+      const shell = el.cloneNode(false);
+      if (thead) {
+        shell.appendChild(thead.cloneNode(true));
+      }
+      const newTbody = document.createElement('tbody');
+      shell.appendChild(newTbody);
+      return shell;
+    };
+
+    return (remaining) => {
+      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));
+        const h = measureFragment(tempTable);
+        if (h <= remaining) {
+          bestCount = i + 1;
+          bestHeight = h;
+        } else {
+          tempBody.removeChild(tempBody.lastChild);
+          break;
+        }
+      }
+
+      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));
+      }
+
+      const rest = createShell();
+      const restBody = rest.querySelector('tbody');
+      for (let i = bestCount; i < relevantRows.length; i++) {
+        restBody.appendChild(relevantRows[i].cloneNode(true));
+      }
+
+      rest._splitInfo = {
+        type: 'table',
+        rowHeights: info.rowHeights,
+        overhead: info.overhead,
+        theadHeight: info.theadHeight,
+        offset: start + bestCount
+      };
+
+      return { first, rest, firstHeight: bestHeight };
+    };
+  }
+
+  // ---------- main pagination loop ----------
   let article_index = 0;
-  while(true){
-    if(article_index === article_seq.length) break;
+  while (article_index < article_seq.length) {
     const article = article_seq[article_index];
-    
-    const raw_element_seq = Array.from(article.children).filter( el => 
-      !['SCRIPT' ,'STYLE' ,'RT-PAGE'].includes(el.tagName)
+
+    const raw_element_seq = Array.from(article.children).filter(el =>
+      !['SCRIPT', 'STYLE', 'RT-PAGE'].includes(el.tagName)
     );
 
-    if(raw_element_seq.length > 0){
-      const page_seq = [];
-      let current_batch_seq = [];
-      let current_h = 0;
+    if (raw_element_seq.length === 0) {
+      article_index++;
+      continue;
+    }
 
-      const get_el_height = (el) => {
-        const rect = el.getBoundingClientRect();
-        const style = window.getComputedStyle(el);
-        const margin = parseFloat(style.marginTop) + parseFloat(style.marginBottom);
-        return (rect.height || 0) + (margin || 0);
-      };
+    const page_seq = [];
+    let current_batch_seq = [];
+    let current_h = 0;
 
-      let i = 0;
-      while(true){
-        if(i === raw_element_seq.length) break;
-        const el = raw_element_seq[i];
-        const h = get_el_height(el);
-
-        if(current_h + h > page_height_limit && current_batch_seq.length > 0){
-          let backtrack_seq = [];
-          let backtrack_h = 0;
-
-          // Backtrack to rescue any widowed headings at the end of the batch
-          while(true){
-            if(current_batch_seq.length === 0) break;
-            const last_el = current_batch_seq[current_batch_seq.length - 1];
-            if(!/^H[1-6]/.test(last_el.tagName)) break;
-            
-            const popped_el = current_batch_seq.pop();
-            backtrack_seq.unshift(popped_el);
-            backtrack_h += get_el_height(popped_el);
-          }
+    let i = 0;
+    while (i < raw_element_seq.length) {
+      const el = raw_element_seq[i];
+      const splitter = isSplittable(el);
+
+      if (splitter) {
+        const remaining = page_height_limit - current_h;
+        const { first, rest, firstHeight } = splitter(remaining);
+
+        if (first) {
+          current_batch_seq.push(first);
+          current_h += firstHeight;
 
-          if(current_batch_seq.length > 0){
+          raw_element_seq.splice(i, 1, rest);
+        } else {
+          if (current_batch_seq.length === 0) {
+            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++;
+          } else {
             page_seq.push(current_batch_seq);
-            current_batch_seq = backtrack_seq;
-            current_h = backtrack_h;
-          }else{
-            // Fallback for an extreme case where the entire page was a cascade of headings
-            page_seq.push(backtrack_seq);
             current_batch_seq = [];
             current_h = 0;
+            raw_element_seq[i] = rest;
           }
         }
-
-        current_batch_seq.push(el);
-        current_h += h;
-
-        i++;
+        continue;
       }
 
-      if(current_batch_seq.length > 0){
-        page_seq.push(current_batch_seq);
-      }
+      const h = get_el_height(el);
 
-      article.innerHTML = '';
-      
-      let p = 0;
-      while(true){
-        if(p === page_seq.length) break;
-        const batch_seq = page_seq[p];
-        const page_el = document.createElement('rt-page');
-        page_el.id = `page-${p + 1}`;
-        
-        let item_idx = 0;
-        while(true){
-          if(item_idx === batch_seq.length) break;
-          page_el.appendChild(batch_seq[item_idx]);
-          item_idx++;
+      if (current_h + h > page_height_limit && current_batch_seq.length > 0) {
+        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);
         }
-        
-        article.appendChild(page_el);
-        p++;
-      }
 
-      if(RT.debug){
-        RT.debug.log('pagination' ,`Article paginated into ${page_seq.length} pages.`);
+        if (current_batch_seq.length > 0) {
+          page_seq.push(current_batch_seq);
+          current_batch_seq = backtrack_seq;
+          current_h = backtrack_h;
+        } else {
+          page_seq.push(backtrack_seq);
+          current_batch_seq = [];
+          current_h = 0;
+        }
       }
+
+      current_batch_seq.push(el);
+      current_h += h;
+      i++;
+    }
+
+    if (current_batch_seq.length > 0) {
+      page_seq.push(current_batch_seq);
+    }
+
+    article.innerHTML = '';
+    let p = 0;
+    while (p < page_seq.length) {
+      const batch = page_seq[p];
+      const page_el = document.createElement('rt-page');
+      page_el.id = `page-${p + 1}`;
+      batch.forEach(item => page_el.appendChild(item));
+      article.appendChild(page_el);
+      p++;
+    }
+
+    if (RT.debug) {
+      RT.debug.log('pagination', `Article paginated into ${page_seq.length} pages.`);
     }
 
     article_index++;
   }
+
+  if (measureContainer && measureContainer.parentNode) {
+    measureContainer.remove();
+    measureContainer = null;
+  }
 };