You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
🥕 multi-assumptions: variant B of 2 parallel framings of the same goal.
Sibling variant: #353. Please compare and tell us which assumption is load-bearing.
A bigger carrot, but pickled in the document model.
My goal
Ship a Plate plugin (@platejs/pagination) that adds print-grade pagination — fixed-size pages (A4 by default) with per-page headers, footers, and footnotes — leveraging chenglou/pretext (text measurement / height oracle, MIT, ESM, v0.0.6) for the line-break and height math that Plate currently lacks.
To do so, do this (variant B — document-model section nodes; pretext as auto-paginator oracle)
Add BaseSectionPlugin, BaseHeaderPlugin, BaseFooterPlugin, BasePageBreakPlugin as sibling base plugins. Header / footer are children of Section, scoped to that section. Manual page_break is a block-level void node. Compose existing packages/footnote/ plugins for the footnote sub-feature.
withPagination editor override (overrideEditor) enforces invariants: top level is always Section[]; non-section content gets wrapped on normalizeNode; nested sections flatten; orphan headers/footers move under their parent section.
Auto-paginator runs as a debounced withNormalizeNode pass: walk a section's children, call pretext.prepare() + layout() to compute cumulative height, insert page_break nodes when content exceeds pageSize.height - margins - headerHeight - footerHeight. Manual page breaks always honoured.
React layer: toTPlatePlugin(BaseSectionPlugin) renders each Section as a stack of fixed-size <div class="page"> between explicit page_break nodes. Header / footer render once per <div class="page">. Footnote definitions stay in document order at the section tail; footnote refs paint a marker; per-page placement of footnote bodies happens via portal driven by the auto-paginator's break positions.
Print mode = no React change. The same <div class="page"> elements + @media print { .page { page-break-after: always; } } produce a deterministic PDF that exactly matches on-screen pagination — pretext-derived breaks are baked into the document tree, not recomputed per print.
Acceptance: opening a 500-block document materializes correct page breaks within 200ms; editing a paragraph re-runs the auto-paginator only on its containing section within 100ms; two Yjs clients with the same editor.children see identical paginations regardless of viewport; DOCX export round-trips section properties + header / footer references with byte-stable w:sectPr.
Because the code is
packages/core/src/lib/plugin/createSlatePlugin.ts:91-159 and :246-259 — createTSlatePlugin + overrideEditor are the canonical pair when the real ownership is editor behaviour and schema invariants.
packages/core/src/lib/plugin/SlatePlugin.ts:56-63 and :151-163 — decorate and inject.nodeProps exist; we don't need them. This variant prefers real schema over decoration.
packages/core/src/lib/plugin/BasePlugin.ts:213-229 (editOnly) — auto-paginator gets editOnly: { handlers: false, normalizeInitialValue: true } so static / read-only renders use last-good break positions without re-running.
packages/table/src/lib/withTable.ts (and friends in packages/table/) — direct precedent: a complex spatial structure managed via withNormalizeNode + dedicated transforms (withInsertFragmentTable, withNormalizeTable, withDeleteTable). Sections are simpler than tables.
packages/footnote/src/lib/BaseFootnoteDefinitionPlugin.ts (and Reference / Input) — composed, not re-authored. Footnote definitions become Section-scoped instead of document-scoped.
.claude/skills/plate-plugin-creator/SKILL.md — Slate-first hard law explicitly satisfied: this variant gives pagination real document semantics and uses React only for chrome rendering.
Google docs - like pagination udecode/plate#4380 — OP danolekh (2025-06-19) shipped a working POC of this approach (BasePaginationPlugin with documentSettings, PageElement with explicit width / height, normalization wrapping non-page nodes); auto-flow was the single weakness, exactly the problem pretext solves.
pretext README (commit bb224e0, 2026-04-22) — pretext as cumulative-height oracle inside withNormalizeNode is its sweet spot: arithmetic-only after prepare(), no DOM reads.
derived per-client view; pagination diverges per viewport, doc converges
deterministic shared pagination, viewport-locked
DOCX round-trip
flat doc + section meta on top-level header / footer siblings
direct mapping to w:sectPr + w:hdrRef / w:ftrRef
Reversibility
feature-flag toggle; turning pagination off is a no-op
schema migration in both directions
editor.children shape
unchanged from non-paginated docs
top-level becomes section[]
Failure mode
over-budget repagination = visible jank, doc still correct
over-budget normalization = stale layout, doc structurally correct
Risk
pretext perf is the load-bearing primitive
break-maintenance transforms on every edit are the load-bearing primitive
Validate this approach — especially but not limited to
Static analysis of packages/table/src/lib/withTable.ts and packages/core/src/lib/plugin/{BasePlugin.ts,SlatePlugin.ts} — confirm withNormalizeNode semantics + editOnly + overrideEditor compose cleanly for Section[] invariants without infinite normalization loops on page_break insertion.
Review of our own code in packages/footnote/ — verify that scoping footnote definitions to a Section instead of the document tail does not break Reference linking or DOCX round-trip.
Comparative analysis vs sibling Plate pagination plugin (headers / footers / footnotes) with pretext (variant A) #353 — name the load-bearing assumption as a single sentence in the form "If <X> matters more than <Y>, variant <A|B> wins; otherwise the other." Specifically: which matters more, deterministic shared pagination + DOCX section fidelity (favours B) or liquid editing UX + zero schema migration (favours A)? Do not pick a winner yet — name the axis.
Stress-test withNormalizeNode cost on the hot path: measure cost of running pretext-driven height accumulation inside normalization on every insert.
Selection / paste behaviour across Section / page_break boundaries — confirm Slate's withInsertFragment handles cross-section pastes without losing structure.
Full plan
New package packages/pagination/ (tsdown, bun test); add to root pnpm-workspace. Run pnpm brl after file scaffolding lands.
Add KEYS.pagination, KEYS.section, KEYS.header, KEYS.footer, KEYS.pageBreak to packages/utils/src/lib/plate-keys.ts.
Public surface: editor.api.pagination.{getSections, getPagesInSection, getPageOf, measure}; editor.tf.pagination.{insertSection, setSectionHeader, setSectionFooter, insertPageBreak, repaginateSection, repaginateAll}. Inference from createTSlatePlugin<Config>; no SlateEditor annotations.
Schema migration: migrateLegacyDocument(doc): SectionDocument for adopters of pre-pagination docs; called once on normalizeInitialValue.
Tests in packages/pagination/src/**/*.spec.ts with bun: invariant maintenance under fuzzed edits, auto-paginator break positions with golden cases, DOCX section round-trip.
DOCX import/export: full w:sectPr mapping; header / footer reference resolution; footnote scoping per section.
Changeset under .changeset/ per .agents/rules/changeset.mdc before completing.
@coderabbitai plan
🥕 multi-assumptions: variant B of 2 parallel framings of the same goal.
Sibling variant: #353. Please compare and tell us which assumption is load-bearing.
A bigger carrot, but pickled in the document model.
My goal
Ship a Plate plugin (
@platejs/pagination) that adds print-grade pagination — fixed-size pages (A4 by default) with per-page headers, footers, and footnotes — leveragingchenglou/pretext(text measurement / height oracle, MIT, ESM, v0.0.6) for the line-break and height math that Plate currently lacks.To do so, do this (variant B — document-model
sectionnodes; pretext as auto-paginator oracle)BasePaginationPlugin(createTSlatePlugin,key: KEYS.pagination) underpackages/pagination/src/lib. Define a top-levelsectionnode schema mirroring DOCXw:sectPr:{ type: 'section', sectPr: { pageSize, margins, columns, header?, footer? }, children: Descendant[] }.editor.childrenbecomesSection[].BaseSectionPlugin,BaseHeaderPlugin,BaseFooterPlugin,BasePageBreakPluginas sibling base plugins. Header / footer are children ofSection, scoped to that section. Manualpage_breakis a block-level void node. Compose existingpackages/footnote/plugins for the footnote sub-feature.withPaginationeditor override (overrideEditor) enforces invariants: top level is alwaysSection[]; non-section content gets wrapped onnormalizeNode; nested sections flatten; orphan headers/footers move under their parent section.withNormalizeNodepass: walk a section's children, callpretext.prepare()+layout()to compute cumulative height, insertpage_breaknodes when content exceedspageSize.height - margins - headerHeight - footerHeight. Manual page breaks always honoured.toTPlatePlugin(BaseSectionPlugin)renders eachSectionas a stack of fixed-size<div class="page">between explicitpage_breaknodes. Header / footer render once per<div class="page">. Footnote definitions stay in document order at the section tail; footnote refs paint a marker; per-page placement of footnote bodies happens via portal driven by the auto-paginator's break positions.<div class="page">elements +@media print { .page { page-break-after: always; } }produce a deterministic PDF that exactly matches on-screen pagination — pretext-derived breaks are baked into the document tree, not recomputed per print.Acceptance: opening a 500-block document materializes correct page breaks within 200ms; editing a paragraph re-runs the auto-paginator only on its containing section within 100ms; two Yjs clients with the same
editor.childrensee identical paginations regardless of viewport; DOCX export round-trips section properties + header / footer references with byte-stablew:sectPr.Because the code is
packages/core/src/lib/plugin/createSlatePlugin.ts:91-159and:246-259—createTSlatePlugin+overrideEditorare the canonical pair when the real ownership is editor behaviour and schema invariants.packages/core/src/lib/plugin/SlatePlugin.ts:56-63and:151-163—decorateandinject.nodePropsexist; we don't need them. This variant prefers real schema over decoration.packages/core/src/lib/plugin/BasePlugin.ts:213-229(editOnly) — auto-paginator getseditOnly: { handlers: false, normalizeInitialValue: true }so static / read-only renders use last-good break positions without re-running.packages/table/src/lib/withTable.ts(and friends inpackages/table/) — direct precedent: a complex spatial structure managed viawithNormalizeNode+ dedicated transforms (withInsertFragmentTable,withNormalizeTable,withDeleteTable). Sections are simpler than tables.packages/footnote/src/lib/BaseFootnoteDefinitionPlugin.ts(and Reference / Input) — composed, not re-authored. Footnote definitions becomeSection-scoped instead of document-scoped.packages/utils/src/lib/plate-keys.ts—KEYS.section,KEYS.header,KEYS.footer,KEYS.pageBreak,KEYS.paginationregistered here..claude/skills/plate-plugin-creator/SKILL.md— Slate-first hard law explicitly satisfied: this variant gives pagination real document semantics and uses React only for chrome rendering.danolekh(2025-06-19) shipped a working POC of this approach (BasePaginationPluginwithdocumentSettings,PageElementwith explicit width / height, normalization wrapping non-page nodes); auto-flow was the single weakness, exactly the problem pretext solves.bb224e0, 2026-04-22) — pretext as cumulative-height oracle insidewithNormalizeNodeis its sweet spot: arithmetic-only afterprepare(), no DOM reads.Assumptions deliberately shuffled vs variant A
section/page_breaknodesheader/footersiblingsw:sectPr+w:hdrRef/w:ftrRefeditor.childrenshapesection[]Validate this approach — especially but not limited to
packages/table/src/lib/withTable.tsandpackages/core/src/lib/plugin/{BasePlugin.ts,SlatePlugin.ts}— confirmwithNormalizeNodesemantics +editOnly+overrideEditorcompose cleanly forSection[]invariants without infinite normalization loops onpage_breakinsertion.packages/footnote/— verify that scoping footnote definitions to a Section instead of the document tail does not break Reference linking or DOCX round-trip.<X>matters more than<Y>, variant<A|B>wins; otherwise the other." Specifically: which matters more, deterministic shared pagination + DOCX section fidelity (favours B) or liquid editing UX + zero schema migration (favours A)? Do not pick a winner yet — name the axis.withNormalizeNodecost on the hot path: measure cost of running pretext-driven height accumulation inside normalization on every insert.Section/page_breakboundaries — confirm Slate'swithInsertFragmenthandles cross-section pastes without losing structure.Full plan
packages/pagination/(tsdown,bun test); add to rootpnpm-workspace. Runpnpm brlafter file scaffolding lands.KEYS.pagination,KEYS.section,KEYS.header,KEYS.footer,KEYS.pageBreaktopackages/utils/src/lib/plate-keys.ts.src/lib/:base-pagination-plugin.ts,base-section-plugin.ts,base-header-plugin.ts,base-footer-plugin.ts,base-page-break-plugin.ts,with-pagination.ts(override + normalize),auto-paginate.ts(pretext-backed cumulative-height pass),internal/font-from-style.ts,internal/measure-cache.ts,internal/section-invariants.ts,types.ts. Helpers ininternal/.src/react/:pagination-plugin.ts(toTPlatePlugin),section-plugin.ts,header-plugin.ts,footer-plugin.ts,page-break-plugin.ts,page-element.tsx,section-element.tsx,header-element.tsx,footer-element.tsx,internal/use-pretext-measurer.ts.packages/footnote/:PaginationPlugin = toTPlatePlugin(BasePaginationPlugin, { plugins: [SectionPlugin, HeaderPlugin, FooterPlugin, PageBreakPlugin, FootnoteDefinitionPlugin, FootnoteReferencePlugin, FootnoteInputPlugin] }).editor.api.pagination.{getSections, getPagesInSection, getPageOf, measure};editor.tf.pagination.{insertSection, setSectionHeader, setSectionFooter, insertPageBreak, repaginateSection, repaginateAll}. Inference fromcreateTSlatePlugin<Config>; no SlateEditor annotations.migrateLegacyDocument(doc): SectionDocumentfor adopters of pre-pagination docs; called once onnormalizeInitialValue.packages/pagination/src/**/*.spec.tswith bun: invariant maintenance under fuzzed edits, auto-paginator break positions with golden cases, DOCX section round-trip.w:sectPrmapping; header / footer reference resolution; footnote scoping per section..changeset/per.agents/rules/changeset.mdcbefore completing.References
packages/core/src/lib/plugin/{createSlatePlugin.ts, SlatePlugin.ts, BasePlugin.ts}packages/core/src/react/plugin/toPlatePlugin.tspackages/table/src/lib/withTable.tsand friends — closest precedent for normalize-driven structural pluginpackages/footnote/src/lib/BaseFootnote*Plugin.ts.claude/skills/plate-plugin-creator/SKILL.mdandrules/{creation-flow,typing,composition}.mdsrc/{layout,line-break,analysis,rich-inline}.tsat commitbb224e0