@sobree/core 0.1.18 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -216,22 +216,4 @@ export declare class PaperStack {
216
216
  * a real "last page with just a signature line" still gets its own
217
217
  * page. Idempotent; safe to call on a single-page result.
218
218
  */
219
- /**
220
- * Walk paginator output forward, absorbing pages whose total content
221
- * height is ≤ 15% of `budgetPx` (the page-content budget) into the
222
- * previous page. These tiny tail-pages are paginator widows — usually
223
- * a 1-line overflow or an LRPB-induced sub-section that didn't have
224
- * room. Both Word and LO tolerate a bit of bottom-margin spill in
225
- * exchange for not spending an entire fresh page on the runt.
226
- *
227
- * Safety: only absorbs pages whose content height is smaller than the
228
- * previous page's slack PLUS the widow tolerance (~20% of budget).
229
- * If absorbing would visibly push content far past the page bottom,
230
- * we leave the runt page alone — better an underfilled page than
231
- * unreadable overflow.
232
- *
233
- * Run AFTER `collapseTrailingEmptyPages` so trailing whitespace is
234
- * already merged and we're working on the real content layout.
235
- */
236
- export declare function collapseUnderfilledPages(pages: readonly HTMLElement[][], budgetPx: number): HTMLElement[][];
237
219
  export declare function collapseTrailingEmptyPages(pages: readonly HTMLElement[][]): HTMLElement[][];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sobree/core",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -1,18 +0,0 @@
1
- import { PaginatedDoc } from './types';
2
- /**
3
- * Apply a PaginatedDoc to the source DOM. The `sourceBlocks` array
4
- * must be the SAME elements (same `data-meas-id` stamps) the measure
5
- * pass walked — the applicator uses the id to resolve a `PageSegment`
6
- * back to the originating element.
7
- *
8
- * Returns one `HTMLElement[]` per page, in the order the page should
9
- * render. The caller (paperStack) appends these into paper-content boxes.
10
- *
11
- * SIDE EFFECTS on the source DOM:
12
- * - Paragraph / list-item splits clone the source element, replacing
13
- * it with a head fragment in place.
14
- * - List / table clones MOVE their LI / TR children out of the source
15
- * into per-page clones; the source container is removed if emptied.
16
- * Source DOM should be treated as consumed after this call.
17
- */
18
- export declare function applyPaginatedDoc(doc: PaginatedDoc, sourceBlocks: readonly HTMLElement[]): HTMLElement[][];
@@ -1,7 +0,0 @@
1
- import { BlockMeasurement, PaginatedDoc, PaginationConstraints } from './types';
2
- /**
3
- * Pure paginator over the new typed contract. Determinism, totality,
4
- * forced-break semantics — all from the existing engine; this wrapper
5
- * just translates.
6
- */
7
- export declare function paginateMeasurements(measurements: ReadonlyArray<BlockMeasurement>, constraints: PaginationConstraints): PaginatedDoc;
@@ -1,9 +0,0 @@
1
- import { BlockMeasurement } from './types';
2
- /**
3
- * Measure a flat list of top-level block elements.
4
- *
5
- * `blocks` is the same `HTMLElement[]` shape `paginateBlocks` takes
6
- * today — caller's responsibility to filter to direct flow children of
7
- * the source paper-content.
8
- */
9
- export declare function measureBlocks(blocks: readonly HTMLElement[]): BlockMeasurement[];
@@ -1,273 +0,0 @@
1
- /**
2
- * Phase 2 paginator contract — the typed boundary between the renderer
3
- * and the pagination engine.
4
- *
5
- * Today's flow (the layer this replaces):
6
- *
7
- * blocks: HTMLElement[]
8
- * → buildItems() (reads live DOM `offsetHeight`,
9
- * stamps `data-pag-*` ids,
10
- * emits an Item[] stream)
11
- * → paginate() (the pure engine — already first-class)
12
- * → distributePages() (mutates DOM: splits <p>/<ol>/<table>)
13
- * → collapse*Pages() (post-process passes that re-read heights
14
- * from blocks in the WRONG DOM context,
15
- * source of the worst recurring bugs)
16
- *
17
- * Why that's wrong (recorded in `packages/core/docs/SESSION_HANDOFF.md`):
18
- * - Forced breaks are smuggled in as `Penalty(-Infinity)` inside the
19
- * Item stream, NOT as first-class constraints. The engine can
20
- * refuse to grow the page array when honouring them would create
21
- * overflow — so a `<w:pageBreakBefore/>` that doesn't fit silently
22
- * shortens the document.
23
- * - Inter-module signalling is ad-hoc: `data-page-break-before`,
24
- * `data-keep-next`, `data-pag-pid`, `data-pag-lid`, `data-pag-tid`,
25
- * `.keep-together`, etc. — none of it typed.
26
- * - The post-process passes re-measure block heights after some
27
- * blocks have moved and others haven't, producing nonsense values
28
- * (a 268px table measured as 36px in firstContent → mistakenly
29
- * classified as a widow → absorbed onto the previous page → 268px
30
- * overflow into the footer band).
31
- *
32
- * The new contract:
33
- *
34
- * BlockMeasurement[] ← computed ONCE from source DOM
35
- * PaginationConstraints ← typed budget + rules
36
- * → purePaginate() ← engine: pure (measurements,
37
- * constraints) → PaginatedDoc
38
- * PaginatedDoc ← partition, NOT a DOM mutation
39
- * → applyPaginatedDoc() ← single DOM pass: splits + moves
40
- *
41
- * Pure. Typed. Forced-breaks-as-boundaries. One DOM mutation pass.
42
- *
43
- * This file is types only. The measurement pass, engine wrapper, and
44
- * DOM applicator land in sibling files in subsequent steps and are
45
- * gated behind a flag until per-fixture verification passes.
46
- */
47
- /**
48
- * One block as the paginator sees it. All quantities are pre-measured;
49
- * the engine NEVER reads the DOM.
50
- *
51
- * Multi-line paragraphs, multi-item lists, multi-row tables are SINGLE
52
- * BlockMeasurements with non-empty `splitPoints` — the engine breaks
53
- * within the block when it chooses one of those points.
54
- */
55
- export interface BlockMeasurement {
56
- /**
57
- * Stable identifier. The engine refers to blocks (and segments within
58
- * blocks) only by id — never by DOM reference. The DOM applicator
59
- * holds the `blockId → HTMLElement` map.
60
- *
61
- * Ids must be stable across pagination retries (footnote-zone budget
62
- * recalc, e.g.). Caller's choice; typically derived from the source
63
- * AST's `block-index` or from the rendered DOM's `data-block-index`.
64
- */
65
- blockId: string;
66
- /**
67
- * Natural rendered height of this block in CSS px when placed in its
68
- * canonical context (the source paper's `.paper-content`). Measured
69
- * once by the measurement pass.
70
- *
71
- * For a splittable block (paragraph with multiple lines, list with
72
- * multiple items, table with multiple rows), this is the height when
73
- * the WHOLE block lands on one page. When the engine chooses to split
74
- * at a `splitPoint`, the page-end segment's height is
75
- * `splitPoint.yOffset`; the next-page segment's height is
76
- * `height - splitPoint.yOffset`.
77
- */
78
- height: number;
79
- /**
80
- * Inter-block gap that appears BEFORE this block in flow — the
81
- * post-margin-collapse vertical distance between the previous block's
82
- * bottom and this block's top. Encoded as Glue in the engine's
83
- * stream; counted against page height only between non-trailing
84
- * blocks (trailing glue discards at page boundaries).
85
- *
86
- * 0 for the first block in the stream.
87
- */
88
- gapBefore: number;
89
- /**
90
- * Points at which the engine may break this block across pages.
91
- * Empty / undefined → the block is monolithic (whole thing moves
92
- * together). For paragraphs, one entry per line break. For lists,
93
- * one per `<li>`. For tables, one per `<tr>`.
94
- *
95
- * The yOffset is the height consumed by the on-page side of the
96
- * split, measured from the block's top edge. The breakpoint after
97
- * the LAST line/item/row is implicit (it's the block's bottom edge)
98
- * and not included.
99
- */
100
- splitPoints?: ReadonlyArray<SplitPoint>;
101
- /**
102
- * The block carries an explicit forced page break BEFORE it
103
- * (`<w:pageBreakBefore/>` on the paragraph, or
104
- * `<w:br w:type="page"/>` in its runs). The engine MUST start a new
105
- * page at this block — and MUST grow the page array if the previous
106
- * page is already full.
107
- *
108
- * This is the field forced-break semantics hang on. The engine
109
- * doesn't read `data-page-break-before` from DOM; it reads this.
110
- */
111
- pageBreakBefore?: boolean;
112
- /**
113
- * Keep on the same page as the NEXT block in the stream. Heading
114
- * styles set this so the heading stays with its first content
115
- * paragraph. If both don't fit, the break moves to BEFORE this
116
- * block.
117
- */
118
- keepWithNext?: boolean;
119
- /**
120
- * Keep all of this block's split points together (i.e. the whole
121
- * block stays on one page). Stronger than monolithic: a
122
- * `keepTogether` block CAN have splitPoints (so the engine knows
123
- * the block's internal structure for cost estimation) but is
124
- * penalised heavily for actually breaking. Used for figures /
125
- * `.keep-together` groups.
126
- */
127
- keepTogether?: boolean;
128
- /**
129
- * Out-of-flow blocks (`position: absolute` / `fixed`) contribute 0
130
- * to the page budget. The engine still emits them in document order
131
- * so the DOM applicator can route them to the right page, but their
132
- * height doesn't compete with in-flow content for space.
133
- *
134
- * Anchored frames are intended to render via the floating layer
135
- * (anchorLayer.ts) and not appear here at all; this flag covers the
136
- * fallback where an out-of-flow block still ended up in the body
137
- * block stream.
138
- */
139
- outOfFlow?: boolean;
140
- }
141
- /**
142
- * A position within a block where the engine may break across a page
143
- * boundary. The block stays as one DOM element when not broken; when
144
- * the engine chooses to break here, the DOM applicator slices it.
145
- */
146
- export interface SplitPoint {
147
- /**
148
- * Height (px) consumed by the on-page portion if the engine breaks
149
- * AT this point. Equivalently: the position of this break within
150
- * the block's natural rendered height.
151
- */
152
- yOffset: number;
153
- /**
154
- * Stable identifier for the on-page segment ending here. For
155
- * paragraphs, the line index. For lists, the `<li>` index. For
156
- * tables, the `<tr>` index. The DOM applicator uses this to know
157
- * where to cut.
158
- */
159
- segmentId: string;
160
- /**
161
- * Cost added if the engine breaks at this point. 0 = neutral,
162
- * `Number.POSITIVE_INFINITY` = forbidden. Used for widow / orphan
163
- * penalties (e.g. forbidding a break that leaves 1 line of a
164
- * 3-line paragraph alone).
165
- */
166
- penalty?: number;
167
- }
168
- /**
169
- * Engine constraints — the budget and rules the engine optimises
170
- * within. Separate from `BlockMeasurement[]` so the same stream can
171
- * be re-paginated under different budgets (footnote-zone recalc)
172
- * without re-measuring.
173
- */
174
- export interface PaginationConstraints {
175
- /**
176
- * Body-area height for page index `i`, in px. The engine uses
177
- * `pageHeights[i]` when present, falling back to
178
- * `defaultPageHeight` for pages beyond the array's length.
179
- *
180
- * Today's paginator computes per-page heights iteratively because
181
- * footnote zones eat budget on specific pages; that signal flows in
182
- * here once, instead of being re-discovered through retry loops.
183
- */
184
- pageHeights: ReadonlyArray<number>;
185
- /** Default body-area height for pages past `pageHeights.length`. */
186
- defaultPageHeight: number;
187
- /** Minimum lines of a paragraph that may sit on the new page. Default 1. */
188
- widows?: number;
189
- /** Minimum lines of a paragraph that may sit on the current page. Default 1. */
190
- orphans?: number;
191
- /**
192
- * Penalty cost added to breaks that would create a widow or orphan
193
- * inside `widows` / `orphans` limits. Default 10000 — high enough
194
- * to win against natural breaks but finite so it doesn't escalate
195
- * to `Infinity` and create unsatisfiable constraints.
196
- */
197
- widowOrphanPenalty?: number;
198
- /**
199
- * Penalty cost added to breaks inside a `keepWithNext` adjacency or
200
- * across a `keepTogether` block. Default 10000.
201
- */
202
- keepPenalty?: number;
203
- }
204
- /**
205
- * The paginator's output: a partition of the input stream into pages,
206
- * expressed as data. NO DOM mutation has happened yet.
207
- *
208
- * The DOM applicator reads this and performs the necessary splits +
209
- * moves in a single pass. Snapshot tests bite at THAT layer.
210
- */
211
- export interface PaginatedDoc {
212
- pages: ReadonlyArray<PaginatedPage>;
213
- /**
214
- * Sum of penalty costs the engine accepted for this partition.
215
- * Diagnostic: a high value means widow/orphan/keep rules were
216
- * violated to satisfy a forced break.
217
- */
218
- totalCost: number;
219
- /**
220
- * Whether the engine had to grow the page array beyond what the
221
- * `pageHeights` array sized. True when a forced break landed on a
222
- * page that wasn't accounted for in the constraints — the caller
223
- * may want to re-measure footnote zones for the new page.
224
- */
225
- grewPageArray: boolean;
226
- }
227
- /**
228
- * One page's assignment, in document order.
229
- */
230
- export interface PaginatedPage {
231
- segments: ReadonlyArray<PageSegment>;
232
- /**
233
- * Total height the engine packed onto this page, excluding trailing
234
- * glue. Useful for the DOM applicator to know how much space the
235
- * placed content needs (e.g. for vertical alignment within the
236
- * paper-content box).
237
- */
238
- usedHeight: number;
239
- }
240
- /**
241
- * One block's contribution to a page. When `range` is absent the
242
- * whole block lands here. When present, only the slice from
243
- * `startSegmentId` (exclusive of any earlier-page slice) to
244
- * `endSegmentId` (last segment on this page) lands here.
245
- *
246
- * Continuation across pages of the same block is recognised by
247
- * matching `blockId` between adjacent pages — both halves carry the
248
- * same id; the segment ids tell the DOM applicator where to cut.
249
- */
250
- export interface PageSegment {
251
- blockId: string;
252
- range?: SegmentRange;
253
- }
254
- export interface SegmentRange {
255
- /** First on-page segment (inclusive). */
256
- startSegmentId: string;
257
- /** Last on-page segment (inclusive). */
258
- endSegmentId: string;
259
- }
260
- /**
261
- * The pure paginator: typed in, typed out, no I/O, no globals.
262
- *
263
- * - Determinism: same inputs ⇒ same `PaginatedDoc` (modulo undefined
264
- * behaviour on tied costs, which the engine MUST resolve in a
265
- * stable, documented way — typically by preferring earlier breaks).
266
- * - Totality: defined for every legal input. Empty `measurements` ⇒
267
- * `{ pages: [], totalCost: 0, grewPageArray: false }`.
268
- * - Forced-break correctness: if any `BlockMeasurement` has
269
- * `pageBreakBefore: true`, the returned page assignment places it
270
- * at the START of some page — never mid-page. The page array
271
- * grows as needed.
272
- */
273
- export type PurePaginate = (measurements: ReadonlyArray<BlockMeasurement>, constraints: PaginationConstraints) => PaginatedDoc;