From: Thomas Walker Lynch
- DOM Reference & Attribute Tracking: The element-specific dictionaries maintain explicit references to active layout mechanics, reducing the need for computationally expensive DOM traversal. For example, a section element maps its underlying `
+
+ The governing principle is that an element
+ This yields the load guard directly. The presence of an element's namespace means that element has loaded, so an element tests for its own key and returns if it finds it.
+
+ 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
+ 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,
+ Two consequences worth stating. Because
+ Registration and scheduling are separate concerns and are now separate structures. An element registers work; the schedule decides when the work runs.
+
+ The phase list is an explicit array rather than being inferred from the keys of the task dictionary. Object key iteration is insertion ordered for string keys and would happen to work, but the schedule is the single most important ordering in the engine and it should be stated in one obvious place rather than emerging from the order in which a dictionary was populated. An explicit list also gives registration something to validate against.
+
+ Validating the phase name matters more than it appears to. Without the check, a misspelled phase either throws at registration time or, worse, silently creates a queue that nothing ever runs; the element then does nothing and reports nothing. With the check the failure is immediate and names both the mistake and the file that made it.
+
+ 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.
+
+ 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
+ The structure does not enforce this. A list preserves insertion order and will satisfy an accidental dependency exactly as silently as a set would have; note that a JavaScript set is also insertion ordered, so switching structures neither creates nor removes the hazard. Independence is a property maintained by discipline.
+
+ Discipline can be given teeth cheaply. In debug builds, shuffle each task list before running it.
+
+ This converts an entire class of silent latent bug into a loud immediate one, at the cost of a few lines that are inert unless the token is enabled. Without it, the first element that quietly depends on another works correctly until something perturbs the load order, at which point the failure appears somewhere unrelated to its cause.
+
+ The whole of the above reduces to a small fixed shape. This is the normative form for an element file.
+
+ The namespace is created immediately after the guard and before anything else, so that the window in which the key exists but the file has not finished is as small as it can be made. Helpers defined in the file body survive because the registered task closes over them; nothing else in the body persists, since the body runs once during parse and never again.
+
+ That last point is the reason state lives in the namespace at all. The file body has already finished before there is a document to inspect, a compiled configuration to read, or pages to number. It is not a place where state can be kept for later, because there is no later occasion on which code in that body will run. What survives is what a longer-lived function captured, or what was written onto a reachable object. The namespace is that reachable object.
+
+ An element with no cross-phase state still creates its namespace, because the namespace is the plug. The term element is the degenerate case: its helper is captured by its task, and its deduplication set is local to a single pass and correctly declared inside the task rather than outside it. It has nothing to store, and it still registers
- By tracking the element's object within the dictionary, the semantic code dynamically references the exact attributes an element was created with. The presence of attributes such as `splitable` are stored directly within the counter's dictionary as a key (assigned an empty string or null value). The existence of the key itself acts as a boolean flag for the pagination engine to evaluate physical breaking limits.
+ 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.
+ One fact governs everything else here.
+ Two consequences follow. Statements after a load call, in the same file, execute before the loaded file does. And
+ This is also why the shared utilities are established inline in the make file rather than being loaded. Anything the make file loaded would not exist by the time the make file finished, so services that must be available to the very next script cannot themselves be loaded; they must be written where they are needed.
+
+ The module guard must key on the resolved path, not the argument as given. The extension is appended after the check would otherwise run, so two spellings of one module would register as two modules.
+
+
+ Debug belongs with the other shared services in the utility object. Moving it there requires care, because of a trap created by deferred loading. The stage manager currently captures the logger eagerly in its file body.
+
+ If the logger is defined in a file that the stage manager itself requests, that file has not run when the capture happens, and the stage manager permanently holds the no-op stub. The pipeline then runs silently forever, and nothing reports the fact. Shared services must be looked up at call time, not captured at load time, unless they are established before the capturing file loads. Both corrections should be applied together: utilities load first, from the make file, ahead of the stage manager; and the stage manager consults
+ With debug and the other services relocated, the make file reduces to a bootstrap: the module set, the load function, and the load calls. That is the right shape for it.
+
+ A document's head names only the locator and a short configuration block. Everything else arrives transitively.
+
+ 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.
+
+ Note what the
<RT·page> boundaries based on height configurations. Modifies the tree aggressively.
+ + 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. +
+ ++ With the schedule externalized, the pipeline itself is one loop rather than six hand-written blocks. Previously two phases were sets and four were single nullable function slots, so the runner carried two shapes of code and a special case for each empty slot. A list of length one expresses a single registered function without the special case. +
+ ++ 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. +
+ ++ The first consequence is that paragraphs need not be split. Splitting a paragraph is a substantial amount of work, especially in the presence of embedded elements, and a paragraph that runs past the nominal page bottom is simply allowed to. It is also pleasanter to read a paragraph that is not interrupted. +
+ ++ The second consequence resolves what would otherwise be a serious instability. Page lengths are not fully known until layout has completed, because counter values and cross reference text are injected late and occupy space. Counter values grow logarithmically, so the individual increments are tiny. But a tiny increment can push a figure onto the following page, which changes that page's number, which changes the length of every cross reference naming it, which can push something else. A small perturbation amplifies through the document. +
+ ++ Rather than allow that cascade, the final phase lengthens the affected page. Growth is local and terminal: no content moves, so no page number changes, so no cross reference changes length, so nothing further is perturbed. The butterfly effect is cut at its source by refusing to relocate anything. +
+ ++ 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. +
++ 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. +
+ ++ The essential separation: mechanics are local, permission is not. +
++ Only the grid element knows how to cut a grid in half. That knowledge is irreducibly local and belongs to the element. But whether a cut is allowed at a given position is a property of the position, computed over the entire chain of enclosing elements. The two questions have different shapes and different homes, and conflating them is what makes the problem look intractable. +
++ 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. +
++ The consequence is that unsplittable is the only possible default, and it requires no defending. It also removes a rule from the engine: not splitting paragraphs ceases to be policy stated somewhere and becomes simply the absence of a paragraph splitter. Nothing in the paginator mentions paragraphs. Should a paragraph splitter ever be written, it works with no change to the engine, because the engine never encoded the exception. +
++ Permission at a single node is the conjunction of three independent conditions. +
+ +
+ A node is
+ 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. +
+ ++ This is what handles the cases that appear to require special treatment. A splittable element inside an unsplittable step scope cannot be cut, because the step scope is impermeable and sits in the chain. A splittable element inside a splittable step scope that is itself inside an impermeable step scope belonging to a different counter also cannot be cut, for exactly the same reason and with no additional machinery. +
+ ++ The multi-counter case dissolves rather than being solved. The conjunction does not ask which counter owns an enclosing step scope; it asks only whether that node is permeable. Counter identity re-enters afterward, when the chain becomes the soft close list, and that list was always permitted to span several machines. +
++ 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. +
+ ++ An atomic element is now merely a barrier with nothing interesting inside it, and needs no separate treatment; moving an unsplittable element whole to the next page is the same operation as migrating a break outward past it. The paginator need know nothing about grids or sections in particular. +
+ ++ The terminating case is supplied by elastic pages. When the break has migrated all the way to the start of the page there is nothing left to relocate, and the correct response is not an error but the page growing to accommodate its content. This is the same mechanism the final phase uses and for the same reason: growth is local and terminal, whereas relocation propagates. A barrier taller than a page is therefore not a failure condition. At most it warrants a message under a debug token when a page exceeds some multiple of the target length, which is a signal about content rather than a fault in the engine. +
++ 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. +
++ With permission handled by the chain, mechanics return to the element that owns them, and the global splitter table disappears. +
+ +
+ The paginator resolves a tag to its element namespace and looks for
+ This also completes the plug-in principle. An element that is not loaded contributes no splitter, occupies no slot in any table, and requires no entry anywhere to record its absence. +
++ 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. +
++ The counter is the engine's only stateful primitive. Nearly every numbered thing in a document is built on it: sections, pages, endnotes, and in due course figures and tables. The user manual documents the four counter tags and their attributes; this section documents the machine underneath them. +
+ ++ 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. +
+ +
+ A
+ The reason the status field exists at all is that enter has no other way to distinguish three cases that are indistinguishable from the DOM alone. Entering from
+ The status names are document flavored. After entering a section but before any subsection, the position is that section's preamble. Once a subsection has closed, the position is between subsections. +
+
+ Both modes run the identical status machine. The entire difference lies in how the count is reported: in scoped mode, when the status is
+ A
+ A
+ Sections are scoped. Figures, when added, will be milestone. +
+
+ The counter stores indices, always zero-based naturals. Styles are pure renderings of an index, and each style is a
+ Numeric and Roman styles are one-based in their display and therefore add one when rendering. Alphabetic styles are zero-based in their display, since A is naturally the zeroth letter, and add nothing. A reader computing an expected value by hand must apply the offset per style, and the offset differs within a single count whenever the style vector mixes families. A count of
+ The style is a vector indexed by depth. Depth zero uses element zero, depth one uses element one, and any depth past the end of the vector uses the last element, so a single trailing style covers unbounded nesting. The
+ 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. +
++ Snapshots clone rather than reference, because the machine continues to mutate after the snapshot is taken. Holding a reference would give every snapshot of a given counter the same final value. Any new code path that stores a machine must clone for the same reason. +
++ Snapshotting an empty counter is an error and is reported to the console; a step is required first. A snapshot that no read resolves is harmless. A read naming a snapshot that does not exist substitutes a visible marker into the output and logs an error. The asymmetry is deliberate: an unused snapshot costs nothing, whereas an unresolved read would otherwise leave a blank that the author might not notice. +
++ 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. +
+ ++ The difficulty arises because counter state is produced by a DOM walk that enters on descent and exits on ascent. Naively cutting a step element in two yields two enters and two exits where the document means one of each, and the counter double advances. +
+ +
+ The rule is therefore stated over the whole stack of open scopes rather than over a single element. When a page split is introduced, all open step scopes are
+ Two conditions govern correctness. First, the continuation open depth must equal the soft close depth. A mismatch means the split lost or invented a level, and it is an error. Second, the lowest level continuation open inherits the count state from the lowest level of the split. That innermost scope is the one holding the live count; the outer levels are structural context that must be rebuilt so that subsequent enters and exits land at the correct depth. +
+ ++ 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. +
+ ++ A consequence worth internalizing is that page splitting affects sections even when the element explicitly being split is not a section. If a list is cut, that list sits inside a section, so the section's step scope is cut as well. Whatever element triggered the split, the entire enclosing stack participates. This is the same chain the permission conjunction walks. +
+ ++ The invariant the protocol maintains is that there is exactly one enter and one exit per logical scope, however many page fragments that scope is spread across. Of N fragments, the first through the N minus first are marked continued; the second through the Nth are marked continuation; only the Nth actually exits. +
++ 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. +
++ Figure scopes are unusual in a second respect worth recording before the element is written: there are long stretches of document between figure scopes. Section scopes tile the document, so a scoped section counter always has a defined answer. Figure scopes are sparse islands, and the question of which figure contains the current position has no answer across most of the text. That is the structural reason figures are a milestone counter rather than a scoped one. Milestone mode answers how many figures have passed, which is defined everywhere, rather than which figure contains the position, which is not. +
+
+ The section element adds no new state machine. It is close to a pure macro: it rewrites each
+ The name child is hidden rather than removed, because the counter walk reads it to populate the names array. Its text reaches the page through the read tag keyed on name, not through its own rendering. A top level section additionally receives a preceding page break if it does not already have one. +
+
+ Beyond serving as the plug,
+ The reason to retain the node itself, rather than a copy of its settings, is that the make tag is the configuration record. Its attributes are the single source of truth for how sections number, and other section-aware tags check them to decide how to behave. A table of contents entry, a cross reference, and a running header all need the section numbering style, and all three should read it from the make node rather than each hard-coding an assumption or duplicating a default. +
+ ++ The element creates the make node only if the document does not already contain one for that counter name. An author wanting non-default section numbering supplies a make tag directly, and the macro defers to it. That deferral is why the node reference and the already-created flag are necessarily the same piece of information rather than two. +
+ +
+ The counter is named
+ 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. +
++ Depth is consumed only by styling. Heading size, indent, and opacity all derive from it. The number itself comes from the counter, not from this walk. Keeping the two independent means a change to the styling rule cannot corrupt numbering. +
++ Collected for quick reference. A defect in the engine is usually one of these being violated, and checking them in order is a faster diagnostic than reading the walk. +
+ ++ 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. +
-<RT·book> element is a planned extension. We are deliberately delaying its separation from the article layout until the core engine's pagination and state machine logic are fully stabilized. Currently, book mechanics operate by triggering page breaks and section scoping within the standard article stream.
+ + Two items are outside this sequence. The figure counter is not yet implemented and should wait until splitting is reworked, since it is the first element that will exercise capability-as-default deliberately. And several test documents author the lowercase counter namespace form; nothing requires this, since the parser and CSS selector matching handle the canonical spelling correctly on their own, so those test documents should be brought to the RT-conforming spelling rather than left as a second accepted form. +
+