From: Thomas Walker Lynch Date: Fri, 7 Aug 2026 05:48:37 +0000 (+0000) Subject: grid now splits along with sections, first time for both together X-Git-Url: https://git.reasoningtechnology.com/%27%20%20%20window.RT.dirpr_library%20%20%20%27/%27%20%20%20key%20%20%20%27?a=commitdiff_plain;h=dd2915bd763c032f6f708549923034ad677195ba;p=RT-Style grid now splits along with sections, first time for both together --- diff --git a/developer/authored/Manuscript.copy/Document/design.html b/developer/authored/Manuscript.copy/Document/design.html index 103ee09..4eb0861 100644 --- a/developer/authored/Manuscript.copy/Document/design.html +++ b/developer/authored/Manuscript.copy/Document/design.html @@ -32,6 +32,10 @@

Code in this manual follows the RT code format conventions. Two of those conventions appear constantly in the engine source and affect reading comprehension: vertical comma lists place the comma at the head of the line it belongs to, and the typographic middle dot serves as an ad hoc namespace separator. Namespaces are written in PascalCase, so the counter element publishes <RT·Counter·make>, not <RT·counter·make>. Author markup, and write queries against it, using the RT-conforming spelling only. The HTML parser lowercases tag names internally as a matter of course, and CSS type selectors are matched case-insensitively in HTML documents, so a query written in the canonical spelling matches regardless of what the parser did to the source markup underneath it. This is harmless precisely because there is no rt namespace of unrelated elements for the lowercase form to collide with. Querying for both spellings, as some element files presently do, is therefore redundant rather than protective.

+

+ Two words are avoided throughout: may and just. Both carry several meanings at once and leave the reader to guess which was intended. May collapses permission, possibility, and probability into one syllable; RFC 2119 spent a clause on the problem and still concluded the word was best avoided. Write what is meant instead: might for possibility, can for capability, has permission to or is permitted to for permission, and a plain assertion where the hedge was doing nothing. Just is the same failure in a smaller compass, meaning variously merely, only, exactly, or recently. A word that needs its context to be understood is not carrying its own weight, and a specification is the last place to rely on the reader reconstructing intent. +

+

Headings capitalize the first word only. This is not house preference but a requirement of the notation: identifiers in this system carry meaning in their case, so RT.Element, RT·Section·counter, and CountingNumber must read the same in a heading as in code. Title casing every word makes an ordinary word indistinguishable from an identifier, and worse, makes a genuine identifier look like ordinary title casing. Sentence case keeps the distinction legible. The same rule applies to headings the engine generates for itself, such as a table of contents title.

@@ -96,11 +100,11 @@

- The invariant that makes this sound: a namespace is created only by its owning element file, in the file body. No other code may create an element's namespace, and the owning file must create it before doing anything else. Were some cooperating element to create the namespace first, presence would become true before the file body had run, and the guard would skip a file that had not in fact loaded. The invariant is what allows bare presence to serve as the flag, with no separate is_loaded member. + The invariant that makes this sound: a namespace is created only by its owning element file, in the file body. No other code creates an element's namespace, and the owning file must create it before doing anything else. Were some cooperating element to create the namespace first, presence would become true before the file body had run, and the guard would skip a file that had not in fact loaded. The invariant is what allows bare presence to serve as the flag, with no separate is_loaded member.

- The invariant also settles how elements communicate. They do not reach into each other's namespaces during load, because during load the other element may not have plugged in yet, and asking would violate the invariant by tempting the asker to create what it did not find. Inter-element communication happens in a later phase, through functions, when every element that is going to load has loaded. By the time the first phase runs, RT.Element is complete and stable, and an element may consult any key it likes, tolerating absence as the normal condition it is. + The invariant also settles how elements communicate. They do not reach into each other's namespaces during load, because during load the other element might not have plugged in yet, and asking would violate the invariant by tempting the asker to create what it did not find. Inter-element communication happens in a later phase, through functions, when every element that is going to load has loaded. By the time the first phase runs, RT.Element is complete and stable, and an element can consult any key it likes, tolerating absence as the normal condition it is.

@@ -152,7 +156,7 @@

- Task lists are lists, not sets. An earlier design used a set, which deduplicated by function identity. That protection is unnecessary, because the module guard and the namespace guard between them already prevent a file from registering twice, and it forbids something legitimate: a task may reasonably be queued more than once when it is genuinely wanted more than once. If a particular task must not be queued twice, that task's own registration can scan the list first; the lists are short and this is the rare case rather than the common one. + Task lists are lists, not sets. An earlier design used a set, which deduplicated by function identity. That protection is unnecessary, because the module guard and the namespace guard between them already prevent a file from registering twice, and it forbids something legitimate: a task might reasonably be queued more than once when it is genuinely wanted more than once. If a particular task must not be queued twice, that task's own registration can scan the list first; the lists are short and this is the rare case rather than the common one.

@@ -305,7 +309,7 @@ Establishes RT.Module and RT.load, then requests the utilities, the stage manager, the theme machinery, the theme manifest, the counter layout, and the note layout. - Establishes RT.Debug, the string, DOM, font, and colour helpers, and the registry helpers. Loaded first among the make file's requests, so that everything after it may rely on the logger existing. + Establishes RT.Debug, the string, DOM, font, and colour helpers, and the registry helpers. Loaded first among the make file's requests, so that everything after it can rely on the logger existing. Creates RT.Element, RT.Phase, RT.Task, RT.task_add, and RT.Registry. Locks the layout by hiding the document element, configures scroll restoration, captures the scroll target, binds window events, and registers the pipeline against DOMContentLoaded. @@ -314,10 +318,10 @@ The theme machinery defines the theme accessor and the preference function. The counter and note layouts plug in and register their tasks. - The author's inline script. Selects a theme, then requests the pagination layout, the layout driver, and any global widgets. Runs only after everything above has completed, which is why it may call the theme preference function directly. + The author's inline script. Selects a theme, then requests the pagination layout, the layout driver, and any global widgets. Runs only after everything above has completed, which is why it can call the theme preference function directly. - Registers the two pagination tasks and the splitting machinery. Requested independently of the layout driver, because a document may legitimately want no pagination at all. + Registers the two pagination tasks and the splitting machinery. Requested independently of the layout driver, because a document might legitimately want no pagination at all. The layout is the driver: it declares which elements the format provides and requests them, and registers configuration compilation into the configure phase. @@ -331,7 +335,7 @@

- The configuration block is ordered by dependency only at its head. The theme must be selected before the layout driver runs, because compiled configuration reads theme values. Pagination and the layout driver are intended to be independent of one another. Widgets such as the theme selector are not part of the document and may be omitted entirely, though the default theme must still be declared, since the layout reads it whether or not a reader is offered a choice. + The configuration block is ordered by dependency only at its head. The theme must be selected before the layout driver runs, because compiled configuration reads theme values. Pagination and the layout driver are intended to be independent of one another. Widgets such as the theme selector are not part of the document and can be omitted entirely, though the default theme must still be declared, since the layout reads it whether or not a reader is offered a choice.

@@ -348,7 +352,7 @@ - Compiles the layout configuration dictionary from the selected theme. Separated from the element phase so that every later task may read it without an implicit ordering assumption. + Compiles the layout configuration dictionary from the selected theme. Separated from the element phase so that every later task can read it without an implicit ordering assumption. Evaluates raw 1D streams. Injects section wrappers, maps counter states, evaluates math tokens, and isolates explicit string payloads. @@ -371,7 +375,7 @@

- The ordering is forced by real dependencies rather than by preference. Generators must expand before pagination, because expansion changes height. Pages must exist before counters run, because a page number is itself a counter and there is nothing to step until the page elements exist. Cross references resolve after counters, because a reference target may contain a counter value. And the final pagination pass runs last, for the reason given under elastic pages below. + The ordering is forced by real dependencies rather than by preference. Generators must expand before pagination, because expansion changes height. Pages must exist before counters run, because a page number is itself a counter and there is nothing to step until the page elements exist. Cross references resolve after counters, because a reference target might contain a counter value. And the final pagination pass runs last, for the reason given under elastic pages below.

@@ -423,7 +427,7 @@ 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. + Whether an element is permitted to 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.

@@ -472,7 +476,7 @@ 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. + 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 is.

@@ -487,7 +491,7 @@ 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. + A break does not always fall inside a scope. It might 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.

This follows from the chain view rather than being a separate rule. A position between two siblings has those siblings nowhere in its enclosing chain; the chain runs from the gap upward through the parent. Soft closing a sibling that merely happens to precede the gap suspends a scope that was never cut. @@ -509,7 +513,7 @@ 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. + Two step scopes of the same counter might 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.

The consequence for implementation is that a scope must record which counter it will exit, not which machine object. Holding a reference taken at enter time applies the exit to a machine that a nested restoration has since displaced; the live machine never receives that exit, and its list is left one level too deep. The next sibling then increments within the stale level rather than at its own, giving one point two where two was meant. @@ -525,13 +529,13 @@ 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. + 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 might 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. + 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 might hold several children of no height at all — make tags, snapshots, whitespace — and emitting it yields a page bearing nothing but its own number.

@@ -662,7 +666,7 @@

- A scoped counter answers the question of which scope contains the current position. When the status is between, the deepest level has closed and no longer applies, so it is dropped. Having just closed subsection 1.2, a reader standing in the body of section 1 is in section 1, not in 1.2. The corollary is that a scoped counter at top level in the between status reports the empty string: the list held one element, it was dropped, and no scope applies. That is the correct answer, not a defect, and the Counter test suite asserts it. + A scoped counter answers the question of which scope contains the current position. When the status is between, the deepest level has closed and no longer applies, so it is dropped. Having closed subsection 1.2 a moment earlier, a reader standing in the body of section 1 is in section 1, not in 1.2. The corollary is that a scoped counter at top level in the between status reports the empty string: the list held one element, it was dropped, and no scope applies. That is the correct answer, not a defect, and the Counter test suite asserts it.

@@ -730,7 +734,7 @@

- All of this proceeds per named counter. An outer scope may belong to a counter with a different name, and that outer counter may have a step scope broken at the same boundary. Each name is soft closed and continuation opened independently, against its own machine. + All of this proceeds per named counter. An outer step scope might belong to a counter with a different name, and that outer counter might have a step scope broken at the same boundary. Each name is soft closed and continuation opened independently, against its own machine.

@@ -827,6 +831,77 @@ + + Debugging +

+ The engine reports through RT.Debug, a token filtered logger. Every message carries a token naming the area it concerns. log and warn emit only when their token is in the active set; error emits always, since an error nobody asked to see is still an error. +

+

+ Only scroll is active by default. Everything else is silent until asked for, from the console, on a running document: +

+ + + RT.Debug.enable('paginate') // turn an area on + RT.Debug.disable('paginate') // turn it off again + RT.Debug.active_tokens // what is currently on + + +

+ The point of tokens is that finishing a debugging session costs nothing. The messages are left in the source and the token is removed from the active set, so the instrumentation survives for whoever needs it next. Pagination in particular has been debugged more than once, and each time the narration has been worth more than the reasoning it replaced. Instrumentation is written to be kept, not to be removed once it has served. +

+ + + + Phase entry and task failure. The first thing to enable when a document renders as though nothing ran. + + + One line per element considered, with its height, the space remaining, the decision taken, and the reason. The starting point for anything to do with page boundaries. + + + Verbose pagination: per child measurement inside a split, and the recursion into divisible children. Noisy, and where the difficult faults are found. + + + Counter machine transitions, snapshot storage, and suspension across page boundaries. + + + Section macro expansion and depth computation. + + + Grid model selection and rendering. + + + Table of contents assembly from expanded step scopes. + + + Cross reference and note resolution. + + + Layout configuration compilation. + + + Theme selection and colour resolution. + + + Scroll target capture and restoration. Active by default. + + + Not a narration but a switch. Randomizes task order within each phase, so that a task quietly depending on another fails at once rather than surviving until something perturbs the load order. + + + +

+ Enabling a token must not change what the engine does. Narration reads state and prints; it decides nothing. The one deliberate exception is shuffle, which exists precisely to change behaviour, and which is named so that nobody enables it by accident while chasing something else. +

+ +

+ A caution, since it costs time to rediscover: opening the developer tools is itself a change to the document. A docked panel narrows the viewport, which changes the width available for layout, which changes wrapping and therefore every measured height. A fault that appears with the console closed and vanishes with it open is more likely a width sensitivity than a Heisenbug in the instrumentation. Undock the panel into its own window and toggle the token again; if the behaviour follows the panel rather than the token, the measurement is depending on something it should not. +

+ +

+ A token names an area, not a file. One area reporting under several tokens is a defect in the instrumentation rather than a feature of it, because a reader who enables the obvious name receives part of the account and has no way to tell that the rest exists. Pagination currently reports under paginate, paginate_v, paginate_0, paginate_1, and pagination; the last three predate the others and want folding into the first two. +

+
+ Invariants

@@ -844,7 +919,7 @@ Any new task. Enforce with the debug shuffle; when a real dependency appears, add a phase. - Any file body that captures a service into a local. Deferred loading means the service may not exist yet. + Any file body that captures a service into a local. Deferred loading means the service might not exist yet. Page splitting. The continued and continuation attributes exist solely to preserve this. @@ -853,7 +928,7 @@ Page splitting across a stack of nested scopes. - Any splitter that decides locally whether it may cut. + Any splitter that decides locally whether it is permitted to cut. The final phase. Relocation reintroduces the cascade that elastic pages exist to prevent. @@ -861,6 +936,9 @@ 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. + + Anything measured in a container rather than in place. A height that follows the width available is not a property of the content, and it changes when a developer panel is docked or a window resized. Resolve such widths once and record them, so that probe and fragment lay out as the original did. + 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. @@ -958,10 +1036,10 @@ Resumption of a suspended scope after a page boundary. Restores state, does not run enter, does not advance the count. - A node through which a page break may pass, being one that satisfies all three gates. Permeability is a property of a single node; permission additionally requires the whole enclosing chain to be permeable. + A node through which a page break is permitted to pass, being one that satisfies all three gates. Permeability is a property of a single node; permission additionally requires the whole enclosing chain to be permeable. - An impermeable node. A barrier does not forbid a break; it relocates the break to just before itself. + An impermeable node. A barrier does not forbid a break; it relocates the break to immediately before itself. Counter mode answering which scope contains the current position. Drops the deepest level when the status is between. diff --git a/developer/authored/Manuscript.copy/Element/grid.js b/developer/authored/Manuscript.copy/Element/grid.js index 6bce8a3..7b6c719 100644 --- a/developer/authored/Manuscript.copy/Element/grid.js +++ b/developer/authored/Manuscript.copy/Element/grid.js @@ -160,6 +160,7 @@ }); container_node.replaceWith(wrapper); + freeze_columns(wrapper); execute_two_pass_measurement(wrapper, options); } @@ -196,6 +197,7 @@ }); container_node.replaceWith(wrapper); + freeze_columns(wrapper); execute_two_pass_measurement(wrapper, options); } @@ -231,7 +233,34 @@ return cell; } + /* Fragments inherit the frozen template through cloneNode, so probe and + fragments lay out as the original did. Where a grid was rendered before + freezing existed, fall back to a live read if the element is still + attached; a detached element with no frozen value cannot be measured + meaningfully, and the splitter declines rather than guessing. */ + function columns_known(el){ + return el.hasAttribute('data-rt-columns-frozen') + || (el.isConnected && window.getComputedStyle(el).gridTemplateColumns !== 'none'); + } + + function pin_columns(source ,target){ + if(!source || !target) return target; + if(source.style.gridTemplateColumns){ + target.style.gridTemplateColumns = source.style.gridTemplateColumns; + }else if(source.isConnected){ + const resolved = window.getComputedStyle(source).gridTemplateColumns; + if(resolved && resolved !== 'none') target.style.gridTemplateColumns = resolved; + } + return target; + } + function split_css_grid(el ,remaining ,measure_fn){ + if(!columns_known(el)){ + window.RT.Debug.warn('grid' + ,'column widths unknown for a detached grid; declining to split rather than ' + + 'measuring against an ambient width.'); + return { first: null ,rest: el ,firstHeight: 0 }; + } const rows = rows_of(el); const keys = Array.from(rows.keys()).sort((a ,b) => a - b); @@ -242,7 +271,7 @@ const header_key = keys[0]; const header_cells = rows.get(header_key); - const probe = el.cloneNode(false); + const probe = pin_columns(el ,el.cloneNode(false)); header_cells.forEach(c => probe.appendChild(place_row(c.cloneNode(true) ,0))); let height = measure_fn(probe); if(height > remaining) return { first: null ,rest: el ,firstHeight: 0 }; @@ -264,13 +293,13 @@ if(taken === 0) return { first: null ,rest: el ,firstHeight: 0 }; if(taken >= keys.length - 1) return { first: null ,rest: el ,firstHeight: 0 }; - const first = el.cloneNode(false); + const first = pin_columns(el ,el.cloneNode(false)); header_cells.forEach(c => first.appendChild(place_row(c.cloneNode(true) ,0))); for(let i = 1; i <= taken; i++){ rows.get(keys[i]).forEach(c => first.appendChild(place_row(c.cloneNode(true) ,i))); } - const rest = el.cloneNode(false); + const rest = pin_columns(el ,el.cloneNode(false)); header_cells.forEach(c => rest.appendChild(place_row(c.cloneNode(true) ,0))); let out_row = 1; for(let i = taken + 1; i < keys.length; i++){ @@ -290,6 +319,32 @@ window.RT.Component = window.RT.Component || {}; window.RT.Component['RT·Grid·css'] = { split: split_css_grid }; + + /* Freeze the resolved column widths onto the wrapper at render time. + + A CSS grid sizes its columns from the content of every row it holds, and + from the width available to it. Both are hazards for pagination. + + A probe holding four of twelve rows resolves different widths than the full + table, so its measured height describes a fragment that will never exist. + And because measurement happens in a container sized from the article, any + change to the available width — a docked developer panel narrowing the + viewport, for one — changes every height and therefore every split + decision. Measurement that depends on ambient width is not deterministic. + + Resolving once, here, and recording the answer in pixels removes both. The + value is written to the inline style, so it survives cloneNode and travels + with fragments that have been detached from the document. + */ + function freeze_columns(wrapper){ + if(!wrapper || !wrapper.isConnected) return; + const resolved = window.getComputedStyle(wrapper).gridTemplateColumns; + if(resolved && resolved !== 'none'){ + wrapper.style.gridTemplateColumns = resolved; + wrapper.setAttribute('data-rt-columns-frozen' ,'true'); + } + } + function execute_two_pass_measurement(wrapper, options) { requestAnimationFrame(() => { if (options.wrap_check) { diff --git a/developer/document/debug_vars.txt b/developer/document/debug_vars.txt new file mode 100644 index 0000000..ce01422 --- /dev/null +++ b/developer/document/debug_vars.txt @@ -0,0 +1,2 @@ +RT.Debug.enable('paginate') // one line per element with its reason +RT.Debug.enable('paginate_v') // per-child measurement inside a split