@stesura/core/sub-editor
Shared machinery behind nested editors (footnotes, endnotes, headers/footers, text boxes). One SubEditorKindConfig per kind drives the inner schema (buildSubEditorSchema), the inner state (createSubEditorState) and the main-to-inner binding plugin (createSubEditorBindingPlugin). Edits flow back to the main editor through forwardInnerSteps.
Functions & values
buildSubEditorSchema
Derives a sub-editor schema from the main one:
doc > <wrapperName> > section_content > blocks.
section_contentis kept so section-scoped styling and plugins work.- The wrapper reuses the main
sectionspec so its CSS vars style the panel like a page area. filterOutNodesstrips what must not appear inside the region.
The content expression is READ from regionNodeName, not restated: a
hard-coded “block+” went stale when tables left the “block” group, and
headers holding a table opened empty and desynced. assertContentParity
makes any remaining gap a construction-time throw.
| Name | Type | Default |
|---|---|---|
base | Schema<any, any> | |
config | Pick<SubEditorKindConfig<K>, "kind" | "wrapperName" | "regionNodeName" | "contentExpr" | "filterOutNodes"> |
createSubEditorBindingPlugin
Main→inner sync plugin for one sub-editor kind, mounted on the MAIN editor.
(Inner→main is forwardInnerSteps, called by useSubEditor in editor-react.)
For every registered sub-editor of the kind (several can be open at once), the region’s position is tracked through each step’s mapping and steps inside it are offset into inner coordinates. Remote collab steps take the same path.
Deferred inner dispatch: appendTransaction runs inside the main
editor’s React setState updater, where dispatching a sub-editor transaction
would be a setState-during-render. Inner transactions are queued and flushed
in a microtask; if the inner state moved since build time (dispatch would
throw “mismatched transaction”), the flush resyncs the whole content instead.
Convergence: after every flushed sync the inner content is compared with the region and resynced on mismatch, so a misbehaving plugin causes at worst a logged resync, never silent drift.
| Name | Type | Default |
|---|---|---|
config | SubEditorKindConfig<K> |
createSubEditorRegistry
Builds the registry of mounted sub-editors of one kind.
Entries are identified by mainEditorId + keyId(key): the registry is
page-global, so two editors open on the same note or header would otherwise
overwrite each other’s entry and cross-write documents.
| Name | Type | Default |
|---|---|---|
keyId | (key: K) => string |
createSubEditorState
Creates the inner EditorState: doc > wrapper(wrapperAttrs) > section_content > blocks.
Returns null and logs when the region cannot be edited; keep the panel closed.
| Name | Type | Default |
|---|---|---|
plugins | Plugin<any>[] | |
schema | Schema<any, any> | |
config | CreateDocConfig | |
wrapperAttrs | Attrs | |
blockContent | Node[] | Fragment |
describeSubEditorFailure
Human-readable form of a build failure, for logs and toasts.
| Name | Type | Default |
|---|---|---|
failure | SubEditorBuildFailure |
findUnrepresentableNodeTypes
Deduplicated names of node types in content the inner schema lacks, e.g.
filterOutNodes entries that reached the region by paste, import or a
remote edit. Empty when the content fits.
| Name | Type | Default |
|---|---|---|
content | Fragment | |
inner | Schema<any, any> |
forwardInnerSteps
Applies a batch of inner transactions to outerTr, offset into main-doc
coordinates. Wrapper-scaffolding steps are skipped; doc-global attr steps
pass through unmapped.
Returns false when a step could not be mapped or applied (the docs have
drifted). The caller must then NOT dispatch outerTr and should call
recoverSubEditorAfterFailedForward.
| Name | Type | Default |
|---|---|---|
__0 | { outerTr: Transaction; outerSchema: Schema<any, any>; transactions: readonly Transaction[]; regionPos: number; logContext: string; } |
getAllSubEditors
Every sub-editor currently mounted under mainEditorId, across all kinds.
Order is registry-creation order, then registration order within a kind.
| Name | Type | Default |
|---|---|---|
mainEditorId | string |
recoverSubEditorAfterFailedForward
Resyncs a sub-editor whose edit could NOT be forwarded (forwardInnerSteps
returned false): it is now ahead of the region by a rejected step, and every
later edit would fail the same way. Needed because the plugin’s convergence
path only runs on OUTER transactions, and a dropped forward produces none.
Deferred to a microtask, as the caller is inside a React dispatch.
| Name | Type | Default |
|---|---|---|
config | SubEditorKindConfig<K> | |
editor | ActiveSubEditor<K> | |
outerState | EditorState |
subscribeSubEditors
Notifies whenever any sub-editor mounts or unmounts, in any kind.
| Name | Type | Default |
|---|---|---|
listener | () => void |
Types
ActiveSubEditor
One mounted sub-editor. key identifies which region of the main doc it is
bound to (e.g. { sectionId, target, variant } for headers/footers,
{ noteRef } for notes).
AnySubEditor
A mounted sub-editor, with its kind-specific key erased.
| Name | Type | Default |
|---|---|---|
view | EditorView | |
mainEditorId | string |
SubEditorBuildFailure
Why a sub-editor could not be built, for the caller to surface to the user.
| Name | Type | Default |
|---|---|---|
reason | "schema" | "unrepresentable" |
SubEditorDocResult
| Name | Type | Default |
|---|---|---|
ok | boolean |
SubEditorKindConfig
Everything that varies between sub-editor kinds (header/footer, footnote, endnote). One config drives ALL the shared machinery: the derived schema, the inner doc/state construction, the outer→inner binding plugin, and the React hooks (inner→outer forwarding, search-match sync).
The shared invariants the machinery relies on:
- Inner doc shape:
doc > wrapper > section_content > blocks(blocks at pos 2). - Main doc region:
… > regionNode > blocks(blocks atregion.pos + 1). - Offset between the two coordinate spaces =
region.pos − 1. - Inner positions below 2 are wrapper scaffolding and NEVER cross the boundary.
SubEditorRegion
A bound region of the main document (header/footer content node, note body node).
| Name | Type | Default |
|---|---|---|
pos | number | |
node | Node |
SubEditorRegistry
The mounted sub-editors of one kind. An ARRAY, since several can be open at
once (a header and a footer editor). SubEditorRegister (editor-react)
registers on mount and unregisters on unmount.
Other exports
| Export | Type | Description |
|---|---|---|
SUB_EDITOR_EXCLUDED_PLUGINS | string[] | Plugin key labels stripped from the main editor’s plugins before mounting any sub-editor. Kinds add more via SubEditorKindConfig.extraExcludedPlugins. |