From: Thomas Walker Lynch
Date: Fri, 7 Aug 2026 02:22:54 +0000 (+0000)
Subject: recovered to the point of working pagination with unsplittable grid
X-Git-Url: https://git.reasoningtechnology.com/?a=commitdiff_plain;h=93dc2096ee66dd2d39e4a7dfc22905acf0f64e9a;p=RT-Style
recovered to the point of working pagination with unsplittable grid
---
diff --git a/developer/authored/Manuscript.copy/Document/design.html b/developer/authored/Manuscript.copy/Document/design.html
index 2ede5bf..dfae61f 100644
--- a/developer/authored/Manuscript.copy/Document/design.html
+++ b/developer/authored/Manuscript.copy/Document/design.html
@@ -25,13 +25,17 @@
- The Global Object
+ The global object
Everything the engine owns hangs off window.RT. There is no other global state and no CSS file. The root has four kinds of member, and keeping the kinds distinct is the main thing to understand about it.
@@ -62,7 +66,7 @@
- Elements Plug In
+ Elements plug in
RT.Element is a dictionary whose keys are element names and whose values are that element's namespace object.
@@ -97,7 +101,7 @@
- Phases and Tasks
+ Phases and tasks
Registration and scheduling are separate concerns and are now separate structures. An element registers work; the schedule decides when the work runs.
@@ -145,7 +149,7 @@
- Order Within a Phase
+ Order within a phase
The design intent is that tasks within a phase are mutually independent. All real ordering is expressed by the phases themselves. When a genuine dependency appears, the response is to add a phase, not to arrange the tasks within one. Phases are cheap; if the sequence requires on_page_load_0 and on_page_load_1, that is a better outcome than an unstated dependency between two entries in one list.
@@ -183,7 +187,7 @@
- The Element File Template
+ The element file template
The whole of the above reduces to a small fixed shape. This is the normative form for an element file.
@@ -228,13 +232,13 @@
- Boot Sequence
+ Boot sequence
Boot and pipeline are two different orderings and are easily confused. Boot is the order in which files load and register their work. The pipeline is the order in which that registered work is invoked, long afterward. This section covers the former.
- Deferred Loading
+ Deferred loading
One fact governs everything else here. RT.load emits a script tag with document.write, and a script written that way does not run immediately. It is inserted into the token stream at the position of the currently executing script and runs only once that script has finished. RT.load is a deferred request, not a synchronous include.
@@ -259,7 +263,7 @@
- Debug Placement
+ Debug placement
RT.Debug is a token filtered logger. Messages are tagged with a token, and log and warn emit only when that token is in the active set. error always emits. Tokens are toggled from the console with RT.Debug.enable and RT.Debug.disable, so a programmer finishing a debugging session removes the token and all of its messages stop without any code being touched.
@@ -280,7 +284,7 @@
- The Sequence
+ The sequence
A document's head names only the locator and a short configuration block. Everything else arrives transitively.
@@ -329,7 +333,7 @@
- Execution Pipeline Schedule
+ Execution pipeline schedule
In the RT-Style framework when a page is rendered, the JS code associated with the semantic elements modifies the DOM. It does so in multiple passes so that page numbering, counters, and cross references function without need for forward referencing or concern about dependencies between them. Consequently what is seen in the element view of the browser debug window will not have a one to one correspondence with the elements found in the document.
@@ -378,7 +382,7 @@
- Elastic Pages
+ Elastic pages
A design decision that shapes the whole of pagination, and that arose from the medium rather than from tradition: the output is a browser, so pages need not be a fixed length. Nothing physical forces a page to end at a particular height. Once that constraint is dropped, several problems that are hard in a fixed-page formatter become easy or vanish.
@@ -398,16 +402,24 @@
This is why the final pagination phase runs last and why it only ever grows pages. Any change to it that relocated content instead would reintroduce the cascade it exists to prevent.
+
+
+ Growth applies during pagination as well as after it. When an element cannot be broken and does not fit, the break migrates outward until it reaches the start of the page, at which point there is nothing left to relocate and the page simply grows. Clipping the element into a scrolling frame is the wrong response twice over: it hides content in a medium that has no need to, and it presents a page whose apparent height is unrelated to what it holds.
+
+
+
+ Whatever a page is given, its running height must account for. This reads as bookkeeping and is not. The running height is what every later decision consults, and a forced page break asks it whether the page holds anything at all. A page carrying an element whose height was never added reports itself empty, so the break is judged redundant and discarded, and the next chapter continues on a page that already appears full to the reader. One omission thus produces two unrelated looking faults: content clipped in one place, and a missing page break in another. Any path that places an element must add its height, including the paths that place an element the ordinary rules could not.
+
- Splitting and Pagination
+ Splitting and pagination
Whether an element may be cut across a page boundary is the most subtle question in the engine, because the answer is not a property of the element. This section sets out the decomposition that makes it tractable.
- Permission and Mechanics
+ Permission and mechanics
The essential separation: mechanics are local, permission is not.
@@ -417,7 +429,7 @@
- Capability Is the Default
+ Capability is the default
An element is splittable exactly when a splitter has been written for it. This is not a policy choice but a fact about what code exists. An element that could in principle be cut cannot be cut unless the function that performs the cut has been provided.
@@ -427,7 +439,7 @@
- Three Gates
+ Three gates
Permission at a single node is the conjunction of three independent conditions.
@@ -450,7 +462,7 @@
- The Chain Conjunction
+ The chain conjunction
A break is legal at a position only if every enclosing node from that position up to the page root is permeable. A single impermeable ancestor vetoes the break, however permeable everything below it may be.
@@ -465,7 +477,7 @@
- A Break Between Siblings Splits the Parent
+ A break between siblings splits the parent
A break does not always fall inside a scope. It may fall in the gap between two sibling scopes, after the first has closed and before the second has opened. In that case neither sibling is cut, and neither should be soft closed. The element that spans the boundary is the parent.
@@ -487,7 +499,7 @@
- Nested Cuts of One Counter
+ Nested cuts of one counter
Two scopes of the same counter may be cut at a single boundary: a section and a subsection within it, both spanning the break. The far side then carries two make tags bearing continues, one per suspension, and each legitimately replaces the live machine for that counter name as it restores its own saved state.
@@ -503,7 +515,20 @@
- Breaks Migrate Outward
+ An indivisible child must not silence its siblings
+
+ When a child cannot be divided, the temptation is to report that the parent cannot be divided either. That is wrong, and its cost is out of all proportion to how reasonable it sounds. The parent may have a dozen further children, every one of them divisible, and reporting failure discards all of them at once. The caller, told only that nothing can be done, places the entire subtree as a single unit.
+
+
+ The visible result is a page carrying the whole tail of a chapter, and the natural reading is that the indivisible child caused it. It did not. An element being larger than a page is not a defect; the defect is abandoning its siblings on account of it. A large table that occupies a page of its own is behaving exactly as intended, and the sections that follow it should go on paginating as though it were not there.
+
+
+ The rule is therefore to make progress rather than to report failure. Take the indivisible child alone, let its page grow to fit, and return everything after it as a remainder to be divided normally. Progress must be measured in height and not in count, since a fragment may hold several children of no height at all â make tags, snapshots, whitespace â and emitting it yields a page bearing nothing but its own number.
+
+
+
+
+ Breaks migrate outward
The reframing that makes the whole thing simple: an impermeable ancestor does not forbid a break, it relocates one. If the break cannot occur inside that ancestor, it occurs before it, and the ancestor moves whole to the next page.
@@ -531,14 +556,14 @@
- One Walk Serves Both
+ One walk serves both
The chain computed to decide permission is, once the position settles, precisely the list of scopes that must be soft closed. The permission walk and the suspension walk are the same walk, performed once, feeding two mechanisms. This is the strongest evidence that the decomposition is correct, and any implementation should preserve it rather than walking twice.
- Splitters Live on Their Elements
+ Splitters live on their elements
With permission handled by the chain, mechanics return to the element that owns them, and the global splitter table disappears.
@@ -563,7 +588,7 @@
- Open Question
+ Open question
The instance attribute currently reads as an opt-in, set affirmatively from counter policy when the section macro constructs a step. Under capability-as-default, capability already expresses intent, so the attribute reads more naturally as a per-instance veto: present and false to forbid what would otherwise be permitted. Either reading is workable and the conjunction is unaffected. The choice should be made before it is copied into a second element.
@@ -577,7 +602,7 @@
- The Count Object
+ The count object
A Count holds three fields. A CounterMachine wraps a Count and adds the rendering configuration: the style vector, the separator, the separator placement, and the mode.
@@ -596,7 +621,7 @@
- The Status Machine
+ The status machine
A scope is one <RT·Counter·step> element. The document walk enters the scope on descent and exits it on ascent. The transition table is small enough to state completely.
@@ -623,7 +648,7 @@
- Scoped and Milestone Modes
+ Scoped and milestone modes
Both modes run the identical status machine. The entire difference lies in how the count is reported: in scoped mode, when the status is between, the last element of the list is dropped from the report.
@@ -642,7 +667,7 @@
- Index and Display
+ Index and display
The counter stores indices, always zero-based naturals. Styles are pure renderings of an index, and each style is a to_ and from_ pair on the machine. This separation is the most error-prone part of the counter, so it is stated flatly here.
@@ -666,7 +691,7 @@
- Snapshot and Read
+ Snapshot and read
A snapshot clones the entire machine into the snapshot dictionary under a name. A read resolves against that dictionary in a second pass, after the whole walk has completed. This decoupling is what allows a read to appear anywhere in the document relative to its snapshot, including earlier. A table of contents reads snapshots taken hundreds of pages later.
@@ -679,7 +704,7 @@
- Splitting Step Scopes Across a Page Boundary
+ Splitting step scopes across a page boundary
This is the most intricate mechanism in the engine. The situation is closely analogous to a message split across packets in telecommunications: the payload is cut, carried separately, and reassembled such that the receiver cannot tell it was ever divided. What makes the counter case harder than a packet is that there is not one open thing at the cut. There is a stack of them.
@@ -722,7 +747,7 @@
- Counters That Do Not Split
+ Counters that do not split
The figure counter, when added, will have no splitter. A figure either fits on the page before the break or on the page after it; there is no meaningful half figure, and under capability-as-default that requires only that no splitter be written.
@@ -733,13 +758,13 @@
- Sections as a Macro over Counters
+ Sections as a macro over counters
The section element adds no new state machine. It is close to a pure macro: it rewrites each <RT·section> into a counter step plus the title furniture, and lets the counter do the counting. Reading it as a macro expansion, rather than as an element with logic of its own, is the correct mental model.
- The Expansion
+ The expansion
<RT·Counter·step counter="RT·Section·counter" splitable="true" id="{snap_id}">
<RT·Counter·snapshot counter="RT·Section·counter" snapshot="{snap_id}">
@@ -757,7 +782,7 @@
- The Section Namespace
+ The section namespace
Beyond serving as the plug, RT.Element.Section holds a reference to the make tag that creates the section counter.
@@ -784,7 +809,7 @@
- Depth Computation
+ Depth computation
The macro computes each section's depth by walking up the DOM and counting ancestors that are either an unexpanded section or an already-expanded step belonging to the section counter. Both forms must be counted, because the node list being iterated is static while the expansion mutates the tree. By the time an inner section is processed, its outer ancestors have already become steps.
@@ -825,6 +850,15 @@
The final phase. Relocation reintroduces the cascade that elastic pages exist to prevent.
+
+ Splitting. Zero height children â make tags, snapshots, whitespace â are not progress. A fragment holding several of them and nothing else yields a page carrying only its own page number.
+
+
+ Overflow and fallback paths, which place elements the ordinary rules could not. An unaccounted placement makes the page report itself empty, and a later forced break is then discarded.
+
+
+ An element marking itself splitable with no splitter registered for its tag is silently atomic. Capability decides; the attribute alone does nothing.
+
Two elements independently choosing the same counter name will silently share one machine.
@@ -838,7 +872,7 @@
- Migration Notes
+ Migration notes
The architecture above is the target. This section records what remains to bring the source to it, in a workable order. Each step is mechanical; the ordering matters because later steps assume earlier ones.
@@ -871,6 +905,9 @@
Record the open scope stack per counter name at a cut and assert the two depths on the far side. Currently a mismatch fails silently and produces incorrect numbering rather than an error.
+
+ Grids register no splitter and no longer claim to be splitable. A grid taller than a page grows its page rather than being cut, which is the intended model rather than a deficiency. Should grids ever be made divisible, the attribute returns together with a registered splitter, not before it.
+
Several element files query both the canonical and lowercase spelling of a tag, for example 'RT·section, rt·section'. This is unnecessary: HTML type selectors match case-insensitively, so the canonical spelling alone already matches markup the parser has lowercased. Other files in the same source, including the grid, article, and memo layouts, already query the canonical spelling only and work correctly. Bring the remainder into line. Low risk, no behavior change, and worth doing whenever a touched file happens to carry the redundant form.
diff --git a/developer/authored/Manuscript.copy/Element/TOC.js b/developer/authored/Manuscript.copy/Element/TOC.js
index db9eeb4..c6e0eef 100644
--- a/developer/authored/Manuscript.copy/Element/TOC.js
+++ b/developer/authored/Manuscript.copy/Element/TOC.js
@@ -1,7 +1,7 @@
/*
Element/TOC.js
Processes tags.
- Iterates nested RT_section boundaries, evaluates tree depth,
+ Iterates nested RT·Section·counter step boundaries, evaluates tree depth,
and duplicates the title payload for automated navigation.
*/
@@ -23,7 +23,7 @@
RT.task_add('element' ,function(){
const debug = window.RT.Debug || { log: function(){} };
- if(debug.log) debug.log('TOC' ,'Generating Table of Contents from expanded section steps');
+ if(debug.log) debug.log('TOC' ,'Generating table of contents from expanded section steps');
const config = window.RT.layout_config || {};
const TOC_seq = document.querySelectorAll('RT·TOC');
@@ -50,7 +50,7 @@
}
const sections = [];
- const all_sections = document.querySelectorAll('RT·counter·step[counter="RT_section"], rt·counter·step[counter="RT_section"]');
+ const all_sections = document.querySelectorAll('RT·counter·step[counter="RT·Section·counter"]');
all_sections.forEach(section => {
let depth = 0;
@@ -58,7 +58,7 @@
while(curr){
const tag = (curr.tagName || '').toLowerCase();
- if(tag === 'rt·counter·step' && curr.getAttribute('counter') === 'RT_section'){
+ if(tag === 'rt·counter·step' && curr.getAttribute('counter') === 'RT·Section·counter'){
depth++;
}
curr = curr.parentElement;
@@ -81,7 +81,7 @@
container.innerHTML = '';
const title = document.createElement('div');
title.className = 'RT·TOC-title';
- title.textContent = start_level === 1 ? 'Table of Contents' : 'Section Contents';
+ title.textContent = start_level === 1 ? 'Table of contents' : 'Section contents';
title.style.textAlign = 'center';
title.style.fontWeight = '600';
title.style.fontSize = '1.5em';
diff --git a/developer/authored/Manuscript.copy/Element/grid.js b/developer/authored/Manuscript.copy/Element/grid.js
index 1e49b65..490a2e0 100644
--- a/developer/authored/Manuscript.copy/Element/grid.js
+++ b/developer/authored/Manuscript.copy/Element/grid.js
@@ -86,7 +86,11 @@
wrapper.style.display = 'grid';
wrapper.style.justifyContent = 'start';
wrapper.className = `RT_grid_container ${options.css_class || ''}`;
- wrapper.setAttribute('splitable', 'true');
+ // No splitter is registered for this wrapper ,so the grid is atomic: a
+ // grid taller than a page grows its page rather than being cut. That is
+ // the intended model. Claiming splitable without a registered splitter
+ // is inert and misleading ,so the claim is not made. Restore it together
+ // with an RT.Splitter entry if grids are ever made divisible.
if (options.delimiters) {
wrapper.style.borderLeft = '2px solid ' + (config.content_main || '#000');
@@ -125,7 +129,11 @@
wrapper.style.maxWidth = '100%';
wrapper.className = `RT_grid_container ${options.css_class || ''}`;
wrapper.style.margin = '1.5rem 0';
- wrapper.setAttribute('splitable', 'true');
+ // No splitter is registered for this wrapper ,so the grid is atomic: a
+ // grid taller than a page grows its page rather than being cut. That is
+ // the intended model. Claiming splitable without a registered splitter
+ // is inert and misleading ,so the claim is not made. Restore it together
+ // with an RT.Splitter entry if grids are ever made divisible.
grid_state.cells.forEach(cell => {
const el = cell.element;
diff --git a/developer/authored/Manuscript.copy/Layout/paginate.js b/developer/authored/Manuscript.copy/Layout/paginate.js
index ac81af9..f8a31b6 100644
--- a/developer/authored/Manuscript.copy/Layout/paginate.js
+++ b/developer/authored/Manuscript.copy/Layout/paginate.js
@@ -16,6 +16,57 @@
const page_conf = (RT.config && RT.config.page) ? RT.config.page : {};
const page_height_limit = page_conf.height_limit || 1000;
+ /* ---------------------------------------------------------------
+ Pagination tracing.
+
+ Splitting is the most intricate part of the engine and the hardest to
+ reason about after the fact ,because the decisions are made against
+ measurements that no longer exist by the time the output is inspected.
+ This narrates what the paginator saw and what it decided.
+
+ RT.Debug.enable('paginate') decisions ,one line per element
+ RT.Debug.enable('paginate_v') verbose: per child measurement inside
+ a split ,which is noisy but is where
+ the difficult faults hide
+
+ Both are inert when their token is off ,beyond a set membership test.
+ --------------------------------------------------------------- */
+
+ let trace_depth = 0;
+
+ function tracing(){ return window.RT.Debug.active_tokens.has('paginate'); }
+ function tracing_verbose(){ return window.RT.Debug.active_tokens.has('paginate_v'); }
+
+ function trace(msg){
+ if(!tracing()) return;
+ window.RT.Debug.log('paginate' ,' '.repeat(trace_depth) + msg);
+ }
+
+ function trace_v(msg){
+ if(!tracing_verbose()) return;
+ window.RT.Debug.log('paginate_v' ,' '.repeat(trace_depth) + msg);
+ }
+
+ // A short human readable handle for an element ,so trace lines identify
+ // which node is being discussed without dumping markup.
+ function el_id(el){
+ if(!el) return '(null)';
+ if(el.nodeType !== Node.ELEMENT_NODE) return '#text';
+ const tag = (el.tagName || '?').toLowerCase();
+ const bits = [];
+ const counter = el.getAttribute && el.getAttribute('counter');
+ if(counter) bits.push('counter=' + counter);
+ if(el.className && typeof el.className === 'string' && el.className.trim()){
+ bits.push('.' + el.className.trim().split(/\s+/)[0]);
+ }
+ if(el.getAttribute && el.getAttribute('continued') === 'true') bits.push('CONTINUED');
+ if(el.getAttribute && el.getAttribute('continuation') === 'true') bits.push('CONTINUATION');
+ // first few words of text ,to make sections recognizable in the log
+ const txt = (el.textContent || '').trim().replace(/\s+/g ,' ').slice(0 ,32);
+ if(txt) bits.push('"' + txt + (txt.length >= 32 ? 'â¦' : '') + '"');
+ return tag + (bits.length ? ' [' + bits.join(' ') + ']' : '');
+ }
+
let measure_container = null;
// 1. DOM Measurement Utilities
@@ -76,6 +127,22 @@
return (remaining) => window.RT.Splitter[(el.tagName || '').toLowerCase()](el ,remaining ,measure_fragment ,is_splittable);
}
+ /* An element may claim splitable and have no splitter registered for its
+ tag. Capability is what actually decides ,so the element is atomic and
+ the claim is inert â but it is almost always a mistake ,and a silent one:
+ the element is then unbreakable ,and if it exceeds a page it falls to the
+ overflow path rather than being cut. Report it once per tag. */
+ if(el.hasAttribute('splitable')){
+ const tag_l = (el.tagName || '').toLowerCase();
+ if(!is_splittable.warned) is_splittable.warned = new Set();
+ if(!is_splittable.warned.has(tag_l)){
+ is_splittable.warned.add(tag_l);
+ window.RT.Debug.warn('paginate'
+ ,"<" + tag_l + "> claims splitable but no splitter is registered for it. "
+ + "Treating as atomic. Register RT.Splitter['" + tag_l + "'] or drop the attribute.");
+ }
+ }
+
const tag = (el.tagName || '').toUpperCase();
if(tag === 'UL' || tag === 'OL'){
const items = Array.from(el.children).filter(c => (c.tagName || '').toUpperCase() === 'LI');
@@ -227,6 +294,7 @@
window.RT.Splitter = window.RT.Splitter || {};
window.RT.Splitter['rt·counter·step'] = function(el ,remaining ,measure_fn ,is_splittable_fn){
+ trace_v('split ' + el_id(el) + ' into ' + remaining + 'px');
const children = Array.from(el.childNodes);
let best_count = 0;
let best_height = 0;
@@ -248,13 +316,20 @@
const frag_height = measure_fn(temp_container);
if(frag_height <= remaining){
+ trace_v(' child ' + i + ' ' + el_id(child) + ': cumulative ' + frag_height
+ + 'px <= ' + remaining + 'px ,keep');
best_count = i + 1;
best_height = frag_height;
}else{
+ trace_v(' child ' + i + ' ' + el_id(child) + ': cumulative ' + frag_height
+ + 'px > ' + remaining + 'px ,cut here');
temp_container.removeChild(temp_container.lastChild);
const child_splitter = child.nodeType === Node.ELEMENT_NODE ? is_splittable_fn(child) : null;
if(child_splitter){
+ trace_v(' child is splittable ,recursing with ' + (remaining - best_height) + 'px');
+ trace_depth++;
const child_split = child_splitter(remaining - best_height);
+ trace_depth--;
if(child_split && child_split.first){
split_child_result = child_split;
best_height += child_split.firstHeight;
@@ -265,8 +340,35 @@
}
}
- if(best_count === 0 && !split_child_result && !forced_break){
- return { first: null ,rest: el ,firstHeight: 0 };
+ if(best_height === 0 && !split_child_result && !forced_break){
+ /* No progress. Either nothing fit at all ,or everything that fit has zero
+ height â make tags ,snapshots ,whitespace â which is not progress.
+
+ Abandoning here (returning no first fragment) hands the whole subtree
+ back to the caller ,whose overflow path places all of it on one grown
+ page ,dragging every following subsection along: that is how the tail
+ of a chapter arrives as a single monster page. Emitting the zero height
+ children as a fragment is no better ,since it produces a page holding
+ nothing but its own page number.
+
+ Take one more child instead ,whatever its size. The page grows just
+ enough to hold it and everything after it flows on normally. Growth is
+ meant to be local ,and this applies it at the smallest scope that needs
+ it rather than the largest.
+
+ Testing height rather than count matters: a fragment may hold several
+ children and still be empty ,which is exactly the case that produced a
+ blank page between two full ones.
+ */
+ if(best_count < children.length){
+ temp_container.appendChild(children[best_count].cloneNode(true));
+ best_height = measure_fn(temp_container);
+ best_count++;
+ trace_v(' -> no progress; taking ' + el_id(children[best_count - 1])
+ + ' whole at ' + best_height + 'px ,remainder flows on');
+ }else{
+ return { first: null ,rest: el ,firstHeight: 0 };
+ }
}
/* Decide whether a remainder exists BEFORE marking the fragment.
@@ -284,6 +386,12 @@
*/
const has_rest = (best_count < children.length) || !!split_child_result || forced_break;
+ trace_v(' -> ' + best_count + ' of ' + children.length + ' children fit'
+ + (split_child_result ? ' ,plus a split child' : '')
+ + (forced_break ? ' ,forced break present' : '')
+ + ' ,has_rest=' + has_rest
+ + (has_rest ? ' (soft close ,scope continues)' : ' (no soft close ,scope intact)'));
+
const first = el.cloneNode(false);
const split_id = 'split_' + Math.random().toString(36).substr(2 ,9);
@@ -374,6 +482,9 @@
let current_h = 0;
let i = 0;
+ trace('=== paginate ' + el_id(article) + ' : ' + raw_element_seq.length
+ + ' top level elements ,page limit ' + page_height_limit + 'px ===');
+
while(i < raw_element_seq.length){
const el = raw_element_seq[i];
const splitter = is_splittable(el);
@@ -403,15 +514,24 @@
const has_interior_break = !!el.querySelector('RT·page-break, RT·page-break-primitive');
if(el_h <= remaining && !has_interior_break){
+ trace(el_id(el) + ': ' + el_h + 'px fits in ' + remaining
+ + 'px remaining -> PLACE WHOLE (break falls after it ,not inside)');
current_batch_seq.push(el);
current_h += el_h;
i++;
continue;
}
+ trace(el_id(el) + ': ' + el_h + 'px vs ' + remaining + 'px remaining'
+ + (has_interior_break ? ' ,has interior page break' : '')
+ + ' -> SPLIT');
+ trace_depth++;
const { first ,rest ,firstHeight } = splitter(remaining);
+ trace_depth--;
if(first){
+ trace(' -> first fragment ' + firstHeight + 'px'
+ + (rest ? ' ,remainder continues on next page' : ' ,NO remainder'));
current_batch_seq.push(first);
current_h += firstHeight;
@@ -433,13 +553,25 @@
continue;
}
}else{
+ trace(' -> NOTHING FITS. Element cannot be broken at this position.');
if(current_h === 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);
+ /* Elastic pages. The break has migrated all the way to the start
+ of the page ,so there is nothing left to relocate. Place the
+ element whole and let the page grow around it.
+
+ Growth is local and terminal: no content moves ,so no page
+ number changes ,so no cross reference changes length ,so
+ nothing further is perturbed. Clipping into a scroll frame
+ instead ,as this once did ,both hid content and â because the
+ frame's height was never added to current_h â left the page
+ looking empty ,so the next forced page break was discarded and
+ the following chapter ran on without its break. */
+ trace(' -> page is empty; PLACE WHOLE and grow page to ' + el_h + 'px');
+ window.RT.Debug.warn('paginate'
+ ,'oversized: ' + el_id(el) + ' is ' + el_h + 'px against a '
+ + page_height_limit + 'px limit and cannot be split. Growing the page.');
+ current_batch_seq.push(el);
+ current_h += el_h;
i++;
}else{
let backtrack_seq = [];
@@ -460,13 +592,16 @@
}else{
current_batch_seq.push(...backtrack_seq);
current_h += backtrack_h;
-
- 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);
+
+ // Same elastic page rule as above: nothing left to relocate ,so
+ // place whole and grow. current_h must include it ,or a later
+ // forced break will read this page as empty and be discarded.
+ trace(' -> cannot emit a page here; PLACE WHOLE and grow page by ' + el_h + 'px');
+ window.RT.Debug.warn('paginate'
+ ,'oversized: ' + el_id(el) + ' is ' + el_h + 'px against a '
+ + page_height_limit + 'px limit and cannot be split. Growing the page.');
+ current_batch_seq.push(el);
+ current_h += el_h;
i++;
}
}
@@ -479,6 +614,8 @@
const is_RT_page_break = tag === 'rt·page-break' || tag === 'rt·page-break-primitive';
if(is_RT_page_break){
+ trace(el_id(el) + ' -> FORCED PAGE BREAK'
+ + (current_h > 0 ? ' ,emitting page' : ' ,page already empty ,ignored'));
if(current_h > 0){
page_seq.push(current_batch_seq);
current_batch_seq = [];
@@ -489,6 +626,8 @@
}
if(current_h + h > page_height_limit && current_h > 0){
+ trace(el_id(el) + ': ' + h + 'px would exceed limit at ' + current_h
+ + 'px used -> MOVE TO NEXT PAGE (atomic ,no splitter)');
let backtrack_seq = [];
let backtrack_h = 0;
@@ -510,6 +649,7 @@
}
}
+ trace(el_id(el) + ': ' + h + 'px -> place (atomic) ,page now ' + (current_h + h) + 'px');
current_batch_seq.push(el);
current_h += h;
i++;
@@ -519,6 +659,8 @@
page_seq.push(current_batch_seq);
}
+ trace('=== ' + page_seq.length + ' pages produced ===');
+
article.innerHTML = '';
global_makes.forEach(make => article.appendChild(make));