From: Thomas Walker Lynch Date: Wed, 5 Aug 2026 13:19:52 +0000 (+0000) Subject: splittable counter scopes X-Git-Url: https://git.reasoningtechnology.com/%27%20%20%20full_path%20%20%20%27?a=commitdiff_plain;h=325e29c31dd943cfb9a061f1576872544d5a8aca;p=RT-Style splittable counter scopes --- diff --git a/developer/authored/Manuscript.copy/Document/manual.html b/developer/authored/Manuscript.copy/Document/manual.html index 8e9a762..3ff0311 100644 --- a/developer/authored/Manuscript.copy/Document/manual.html +++ b/developer/authored/Manuscript.copy/Document/manual.html @@ -49,7 +49,7 @@

- For RT-Style to work, the document will need to include a file that points it at the library, as discussed later in this manual. + For RT-Style to work, the document will need to include a file that points it at the library, as discussed in the section on page .

@@ -82,7 +82,7 @@ - Article Environment + Article Environment

The standard operational header template for an article instance:

@@ -234,13 +234,21 @@ Initializes the named counter state machine.
+
Attributes:
+ counter: Required state identifier.
- style: Single style string, or comma-separated hierarchy defining formats at each nesting depth. The terminal format applies to all deeper nestings. Valid arguments: "NaturalNumber", "CountingNumber", "Roman", "roman", "Alpha", "alpha". The "outline" flag translates to a standard mixed document array. (Default: "NaturalNumber")
- on-first-step: The initial base value. (Default: "0")
- separator: String appended between depth sequence levels. (Default: ".")
- separator-placement: "embedded" or "embedded-after". (Default: "embedded")
- mode: "scoped" (content embedded between count boundaries receives the parent scope's value) or "milestone" (state carries forward sequentially irrespective of lexical depth). + +
style: Single style string, or comma-separated hierarchy defining formats at each nesting depth. The terminal format applies to all deeper nestings. Valid arguments: "NaturalNumber", "CountingNumber", "Roman", "roman", "Alpha", "alpha". The "outline" flag translates to a standard mixed document array. (Default: "NaturalNumber")
+ +
on-first-step: The value returned the first time the counter is stepped. The default is style specific: "NaturalNumber"(0), "CountingNumber"(1), "Roman"(I), "roman"(i), "Alpha"(A), "alpha"(a).
+ +
separator: String appended between depth sequence levels. (Default: ".")
+ +
separator-placement: "embedded" or "embedded-after". (Default: "embedded")
+ +
mode: "scoped" (content embedded between count boundaries receives the parent scope's value) or "milestone" (state carries forward sequentially irrespective of lexical depth). +
@@ -263,7 +271,7 @@

Attributes:
snapshot: Required dictionary assignment key.
- key: Sub-state query property. Resolves dot-notation parameters (e.g., "count.status", "count.list"). Querying "count" triggers the style formatter and separator join parameters. (Default: "count") + key: Sub-state query property. Resolves dot-notation parameters (e.g., "count.status", "count.list"). Querying "count" triggers the style formatter and separator join parameters. (Default: "count"). Reading an empty counter (before a first step) causes an error to the console.
diff --git a/developer/authored/Manuscript.copy/Layout/counter.js b/developer/authored/Manuscript.copy/Layout/counter.js index 7c9562d..270fb7c 100644 --- a/developer/authored/Manuscript.copy/Layout/counter.js +++ b/developer/authored/Manuscript.copy/Layout/counter.js @@ -1,7 +1,7 @@ /* Processes tags. Calculates numbering, maintains the explicit status machine, manages snapshots, and outputs values for read tags. - Supports two modes: 'scoped' (default) and 'milestone'. + Supports 'scoped' and 'milestone' modes. Includes DOM continuation suspension architecture. */ (function() { @@ -18,7 +18,8 @@ RT.Counter = RT.Counter || {}; RT.dict_instance = RT.dict_instance || {}; RT.dict_snapshot = RT.dict_snapshot || {}; - + RT.dict_serial = RT.dict_serial || {}; + RT.serial_id_allocator = RT.serial_id_allocator || 1; class Count { constructor() { @@ -45,7 +46,6 @@ } } - read(...path) { if (path.length === 0) return undefined; const key = path[0]; @@ -125,7 +125,7 @@ if (this.list && this.list.length > 0) { const val = this.list.pop(); if (this.list.length === 0) { - this.list = null; // Revert to strict null state if emptied + this.list = null; } return val; } @@ -138,7 +138,6 @@ c.list = this.list ? [...this.list] : null; return c; } - } class CounterMachine { @@ -147,7 +146,7 @@ this.style = ['NaturalNumber']; this.separator = '.'; this.separator_placement = 'embedded'; - this.mode = 'scoped'; // 'scoped', 'milestone' + this.mode = 'scoped'; if (config) { this.write(config); @@ -176,7 +175,6 @@ } this.style = parsed; } else if (key === 'Count' || key === 'count') { - // Support copying from another CounterMachine or Count object const source_count = value instanceof CounterMachine ? value.count : (value instanceof Count ? value : null); if (source_count) { this.count = source_count.clone(); @@ -193,11 +191,10 @@ const current_status = this.count.read('status'); if (current_status === 'empty') { - // Transition state strictly before mutating the list this.count.write('status', 'preamble'); this.count.push(first_step_val !== undefined ? first_step_val : 0); } else if (current_status === 'preamble') { - this.count.push(0); // indent appends 0 + this.count.push(0); this.count.write('status', 'preamble'); } else if (current_status === 'between') { this.count.increment(); @@ -222,7 +219,10 @@ if (!count_obj) return ''; const status = count_obj.read('status'); - if (status === 'empty') return ''; + if (status === 'empty') { + console.error("RT-Manuscript Layout Error: Attempted to output an uninitialized empty counter."); + return '[Empty Counter]'; + } let active_list; if (this.mode === 'scoped' && status === 'between') { @@ -231,7 +231,6 @@ active_list = count_obj.read('list'); } - // Safety check catches null or empty arrays if (!active_list || active_list.length === 0) { return ''; } @@ -249,8 +248,6 @@ return count_str; } - // --- Atomic Type Conversions --- - to_NaturalNumber(num) { return num.toString(); } from_NaturalNumber(val) { const n = parseInt(val, 10); @@ -267,7 +264,7 @@ from_roman(val) { return this.from_Roman(val.toUpperCase()); } to_Roman(num) { - let n = num + 1; // 0-indexed to 1-indexed + let n = num + 1; if (n < 1) return n.toString(); const lookup = {M:1000, CM:900, D:500, CD:400, C:100, XC:90, L:50, XL:40, X:10, IX:9, V:5, IV:4, I:1}; let roman = ''; @@ -294,7 +291,7 @@ i++; } } - return Math.max(0, num - 1); // 1-indexed to 0-indexed + return Math.max(0, num - 1); } to_Alpha(num) { return String.fromCharCode(65 + num); } @@ -335,34 +332,48 @@ if (tag === 'rt·counter·make') { const name = node.getAttribute('counter'); if (name) { - const style_attr = node.getAttribute('style'); - const parsed_style = style_attr ? style_attr.split(',').map(s => s.trim()) : ['NaturalNumber']; + const continues_id = node.getAttribute('continues'); - RT.dict_instance[name] = new CounterMachine({ - style: parsed_style, - separator: node.getAttribute('separator') || '.', - separator_placement: node.getAttribute('separator-placement') || 'embedded', - mode: node.getAttribute('mode') || 'scoped' - }); - - const on_first_step_str = node.getAttribute('on-first-step'); - if (on_first_step_str) { - const top_style = RT.dict_instance[name].style[0]; - const method_name = `from_${top_style}`; - const active_machine = RT.dict_instance[name]; - const initial_val = typeof active_machine[method_name] === 'function' - ? active_machine[method_name](on_first_step_str) - : active_machine.from_NaturalNumber(on_first_step_str); - - active_machine.first_step_val = initial_val; + if (continues_id && RT.dict_serial[continues_id]) { + RT.dict_instance[name] = RT.dict_serial[continues_id].clone(); + } else { + const style_attr = node.getAttribute('style'); + const parsed_style = style_attr ? style_attr.split(',').map(s => s.trim()) : ['NaturalNumber']; + + RT.dict_instance[name] = new CounterMachine({ + style: parsed_style, + separator: node.getAttribute('separator') || '.', + separator_placement: node.getAttribute('separator-placement') || 'embedded', + mode: node.getAttribute('mode') || 'scoped' + }); + + const on_first_step_str = node.getAttribute('on-first-step'); + if (on_first_step_str) { + const top_style = RT.dict_instance[name].style[0]; + const method_name = `from_${top_style}`; + const active_machine = RT.dict_instance[name]; + const initial_val = typeof active_machine[method_name] === 'function' + ? active_machine[method_name](on_first_step_str) + : active_machine.from_NaturalNumber(on_first_step_str); + + active_machine.first_step_val = initial_val; + } } + + const serial = node.getAttribute('serial') || String(RT.serial_id_allocator++); + node.setAttribute('serial', serial); + RT.dict_serial[serial] = RT.dict_instance[name]; } } else if (tag === 'rt·counter·step') { const name = node.getAttribute('counter'); + const is_continuation = node.getAttribute('continuation') === 'true'; + if (name && RT.dict_instance[name]) { const active_machine = RT.dict_instance[name]; - active_machine.enter(active_machine.first_step_val); - active_machine.first_step_val = undefined; // consume it + if (!is_continuation) { + active_machine.enter(active_machine.first_step_val); + active_machine.first_step_val = undefined; + } machine_to_exit = active_machine; } } else if (tag === 'rt·counter·snapshot') { @@ -387,7 +398,16 @@ } if (machine_to_exit) { - machine_to_exit.exit(); + const is_continued = node.getAttribute('continued') === 'true'; + if (!is_continued) { + machine_to_exit.exit(); + } else { + // Cache the suspended state dynamically via the paginator's injected ID + const split_id = node.getAttribute('split-id'); + if (split_id) { + RT.dict_serial[split_id] = machine_to_exit.clone(); + } + } } } @@ -428,10 +448,6 @@ } }; - //------------------------------------------ - // on module load - // - window.RT.counter = counter; })(); diff --git a/developer/authored/Manuscript.copy/Layout/paginate.js b/developer/authored/Manuscript.copy/Layout/paginate.js index fd4d564..8f105cf 100644 --- a/developer/authored/Manuscript.copy/Layout/paginate.js +++ b/developer/authored/Manuscript.copy/Layout/paginate.js @@ -65,17 +65,24 @@ return h; } -// ========================================================= + // ========================================================= // Splitting Logic // ========================================================= function isSplittable(el){ + // Component Dictionary Execution const componentId = el.getAttribute('data-rt-component'); if (componentId && window.RT.Component && window.RT.Component[componentId] && window.RT.Component[componentId].split) { return (remaining) => window.RT.Component[componentId].split(el, remaining, measureFragment); } + // Custom RT splitable attribute delegation + if (el.hasAttribute('splitable') && window.RT.Splitter && window.RT.Splitter[el.tagName.toLowerCase()]) { + return (remaining) => window.RT.Splitter[el.tagName.toLowerCase()](el, remaining, measureFragment, isSplittable); + } + // Native HTML Fallbacks + const tag = el.tagName; if(tag === 'UL' || tag === 'OL'){ const items = Array.from(el.children).filter(c => c.tagName === 'LI'); @@ -224,6 +231,85 @@ } + // ========================================================= + // RT ELEMENT SPLITTERS + // ========================================================= + window.RT.Splitter = window.RT.Splitter || {}; + + window.RT.Splitter['rt·counter·step'] = function(el, remaining, measureFn, isSplittableFn) { + const children = Array.from(el.children); + let bestCount = 0; + let bestHeight = 0; + const tempContainer = el.cloneNode(false); + let splitChildResult = null; + let forcedBreak = false; + + for (let i = 0; i < children.length; i++) { + const child = children[i]; + + // Break on explicit splitting break + if (child.tagName && child.tagName.toLowerCase() === 'rt·page-break') { + forcedBreak = true; + bestCount = i; + break; + } + + tempContainer.appendChild(child.cloneNode(true)); + const fragHeight = measureFn(tempContainer); + + if (fragHeight <= remaining) { + bestCount = i + 1; + bestHeight = fragHeight; + } else { + tempContainer.removeChild(tempContainer.lastChild); + const childSplitter = isSplittableFn(child); + if (childSplitter) { + const childSplit = childSplitter(remaining - bestHeight); + if (childSplit && childSplit.first) { + splitChildResult = childSplit; + bestHeight += childSplit.firstHeight; + bestCount = i; + } + } + break; + } + } + + if (bestCount === 0 && !splitChildResult && !forcedBreak) { + return { first: null, rest: el, firstHeight: 0 }; + } + + const first = el.cloneNode(false); + first.setAttribute('continued', 'true'); + const splitId = 'split_' + Math.random().toString(36).substr(2, 9); + first.setAttribute('split-id', splitId); + + for (let i = 0; i < bestCount; i++) { + first.appendChild(children[i].cloneNode(true)); + } + if (splitChildResult) first.appendChild(splitChildResult.first); + + let rest = null; + if (bestCount < children.length || splitChildResult || forcedBreak) { + rest = el.cloneNode(false); + rest.setAttribute('continuation', 'true'); + if (splitChildResult && splitChildResult.rest) rest.appendChild(splitChildResult.rest); + + const startIndex = forcedBreak ? bestCount + 1 : (splitChildResult ? bestCount + 1 : bestCount); + for (let i = startIndex; i < children.length; i++) { + rest.appendChild(children[i].cloneNode(true)); + } + + const makeTag = document.createElement('rt·counter·make'); + makeTag.setAttribute('counter', el.getAttribute('counter')); + makeTag.setAttribute('continues', splitId); + + rest = [makeTag, rest]; + } + + return { first, rest, firstHeight: bestHeight }; + }; + // ========================================================= // PAGINATE 0: CHUNKING & INJECTING STRUCTURE // ========================================================= @@ -287,9 +373,19 @@ current_h += firstHeight; if(rest){ - raw_element_seq.splice(i ,1 ,rest); + if (Array.isArray(rest)) { + raw_element_seq.splice(i, 1, ...rest); + } else { + raw_element_seq.splice(i, 1, rest); + } + // Force page boundary push because element spanned boundary + page_seq.push(current_batch_seq); + current_batch_seq = []; + current_h = 0; + continue; } else { - raw_element_seq.splice(i ,1); + raw_element_seq.splice(i, 1); + continue; } } else { if(current_batch_seq.length === 0){ @@ -339,9 +435,10 @@ const h = getElHeight(el); const is_RT_page_break = el.tagName && el.tagName.toLowerCase() === 'rt·page-break'; + const is_RT_page_break_primitive = el.tagName && el.tagName.toLowerCase() === 'rt·page-break-primitive'; // Explicit Page Break Logic - Execute immediately without backward traversal - if(is_RT_page_break){ + if(is_RT_page_break || is_RT_page_break_primitive){ if(current_batch_seq.length > 0){ page_seq.push(current_batch_seq); current_batch_seq = []; diff --git a/developer/experiment/RT-Manuscript_locator.js b/developer/experiment/RT-Manuscript_locator.js new file mode 100644 index 0000000..4a1b936 --- /dev/null +++ b/developer/experiment/RT-Manuscript_locator.js @@ -0,0 +1,37 @@ +/* + direct.js + + + We have four scenarios + + immediate - used in the RT-style distribution itself (authored, consummer, staged) + direct - used in the RT-style project itself, but not in the distribution + indirect - the version all Harmony projects use + URL_only - always pulls style through a URL, a webserver must be present + +*/ + +window.RT = window.RT || {}; + +(function() { + const project_name = "RT-Style"; + const path = window.location.pathname; + const project_root_index = path.indexOf('/' + project_name + '/'); + + if (project_root_index !== -1) { + // substring(0, x) excludes the trailing slash. We must prepend it to the payload. + const absolute_project_root = path.substring(0, project_root_index + project_name.length + 1); + window.RT.dirpr_library = absolute_project_root + "/consumer/Manuscript"; + } else { + // Fallback for when served via local Python HTTP daemon from the project root + window.RT.dirpr_library = "../consumer/made/Manuscript"; + } + + document.write( + ' + + + + + + + +

Page 1: Initial Scope

+

The state machine initializes. Scopes 1, 3, and 4 breach the page boundary and are suspended (continued="true").

+ + + + + +

Node 1 execution: (Expected: I)

+ + + +

Node 2 execution: (Expected: I.A) - Closes normally

+
+ + + +

Node 3 execution: (Expected: I.B)

+ + + +

Node 4 execution: (Expected: I.B.i)

+

--- Page Boundary Reached ---

+
+
+
+ + + + +

Page 2: First Continuation

+

The graph is reassembled. Node 4 closes. Node 5 executes. The page boundary is breached again, forcing a second suspension of the outer scopes 1 and 3.

+ + + + + + + + + + + + +

Node 4 continued payload. Current state: (Expected: I.B.i) - Closes normally

+
+ + + +

Node 5 execution: (Expected: I.B.ii) - Closes normally

+

--- Page Boundary Reached ---

+
+ +
+
+ + + + +

Page 3: Second Continuation

+

The final graph reassembly. Node 3 closes. Node 6 executes. Node 1 closes.

+ + + + + + + + + +

Node 3 continued payload. Current state: (Expected: I.B) - Closes normally

+
+ + + +

Node 6 execution: (Expected: I.C) - Closes normally

+
+ + +

Node 1 continued payload. Current state: (Expected: I) - Closes normally

+ +
+ +
+ + diff --git a/tester/authored/Counter/test_4.html b/tester/authored/Counter/test_4.html new file mode 100644 index 0000000..ec7bb0d --- /dev/null +++ b/tester/authored/Counter/test_4.html @@ -0,0 +1,42 @@ + + + + + Counter Split Continuation Test + + + + + + + +

Page 1: Pre-Split

+

This counter scope initiates normally and is deliberately severed by an explicit structural split break.

+ + + + + +

Pre-split evaluation: (Expected: 1)

+ + + +

Page 2: Post-Split Continuation

+ +

Post-split evaluation: (Expected: 1)

+

The array extent remains static across the boundary because the continuation flag successfully suppressed the step increment.

+ + + +

Nested step evaluation: (Expected: 1.A)

+
+ +
+ +
+ +