Skip to Content
PackagesReferences

References

Footnotes, endnotes and cross-references.

PackageContents
@stesura/referencesPlugins, commands, helpers and the note-panel store. Framework-free.
@stesura/references-reactreferenceUiExtensions: toolbar groups, note panels, the cross-reference panel and menus, anchor node views.

The nodes themselves (footnote_anchor, cross_reference, …) are part of the core schema, so stesuraSchema() already has them. See Schema.

Setup

import { referencesPlugins } from "@stesura/references"; import { referenceUiExtensions } from "@stesura/references-react"; import { StesuraEditor, useLocalEditor } from "@stesura/editor-react"; const uiExtensions = [referenceUiExtensions]; const Editor = () => { const { editorState, dispatch, pluginFactory, schema } = useLocalEditor({ extraPlugins: () => referencesPlugins(), }); return ( <StesuraEditor state={editorState} dispatchTransaction={dispatch} schema={schema} pluginFactory={pluginFactory} uiExtensions={uiExtensions} /> ); };

referencesPlugins() returns every plugin the feature needs, already in the right order: cross-reference resolution, cross-reference activation, note numbering and the two note-panel bindings. Mount it after the core numbering plugins (as extraPlugins does), so cross-references see counters updated in the same transaction.

To number notes without cross-references or the note panels, mount notesPlugin() on its own.

Footnotes and endnotes

Both kinds work the same way. Only where they render differs, and endnotes take their number format from the document’s endnotesOptions.

  • Anchor. An inline footnote_anchor / endnote_anchor in the text, carrying the note’s ref. Several anchors can share a ref; they share its number.
  • Body. A footnote_body / endnote_body holding the note’s content, stored in a document-level container (footnotes, or endnotes, which is created on the first endnote). Bodies are edited in the note panel, never inline.

The notes plugin numbers anchors in document order and keeps bodies matched to them. It creates a missing body, deletes orphaned or duplicate bodies, and gives a ref to anchors that arrive without one (DOCX import, stripped paste).

Notes are not allowed in headers or footers (as in Word, a header repeats on every page), nor inside another note’s body. The sub-editors refuse them. Anything that slips through a paste, import or collab commit is removed by the plugin, and its body goes with it.

Editing

The note panel opens at the bottom of the document on:

  • a double-click, Ctrl/Cmd+Click, or Enter/Space on an anchor;
  • inserting a note;
  • the toolbar’s “open” buttons (References and View tabs), which open the note after the cursor;
  • a search match inside a note body.

The panel shows one note at a time, with Previous/Next, Go to anchor (scrolls the main editor without moving focus) and Delete. It closes on Escape or a click on the overlay. Only one panel, footnote or endnote, is open per editor.

In view mode the open buttons stay enabled and the panel is read-only.

Commands

import { deleteFootnote, insertFootnote, navigateFootnote, nextFootnote, previousFootnote, } from "@stesura/references";

Endnotes have the same five: insertEndnote, deleteEndnote, nextEndnote, previousEndnote, navigateEndnote.

CommandSignatureBehaviour
insertFootnote(ref?, onSuccess?) => CommandInserts an anchor at the cursor, or at the end of a non-empty selection (kept, not replaced). Omit ref to create a new note; pass one to add another anchor to an existing note. onSuccess(ref) runs on dispatch only. False in a code block, on a selected table or a gap cursor.
deleteFootnote(ref) => CommandDeletes the body and every anchor of the note in one undo step.
nextFootnote(onSuccess?) => CommandSelects the next anchor after the cursor, wrapping around. The probe is true whenever the document has a footnote.
previousFootnote(onSuccess?) => CommandThe same, backwards.
navigateFootnote(ref, 1 | -1, onNavigate?) => CommandSelects the note after/before ref in numbering order. Doesn’t wrap: false at either end. Drives the panel’s Previous/Next.

nextFootnote and previousFootnote move the selection only. They don’t open the panel.

Reading note state

import { getEndnotes, getFootnotes } from "@stesura/core/plugins"; const footnotes = getFootnotes(view.state); // FootnoteItem[], in numbering order const endnotes = getEndnotes(view.state); // EndnoteItem[]

Each item has the note’s ref, index (0-based), occurrences and the sectionId of its first anchor. The arrays keep their identity until the notes change, so they are safe as useSyncExternalStore snapshots.

Panel state

Which note is open is held per editor in nanostores, exported from @stesura/core/stores:

import { activeNoteRefAtom, closeNotePanel, openNotePanel } from "@stesura/core/stores"; openNotePanel(editorId, "footnote", ref); // also closes the endnote panel openNotePanel(editorId, "footnote", ref, { focusOnOpen: false }); // keep focus where it is closeNotePanel(editorId, "footnote"); activeNoteRefAtom(editorId, "endnote"); // atom<string | null>, stable per editor + kind

You only need the rest for a custom panel shell: noteFocusRequestAtom (consumed by <FocusOnRequest> from @stesura/editor-react), footnoteEditorRegistry / endnoteEditorRegistry, and the sub-editor kinds footnoteKind / endnoteKind.

Cross-references

A cross-reference is an inline cross_reference node pointing at another node’s id. The target can be any node type carrying the numbering global attribute (headings and paragraphs by default), numbered or not, as long as it’s outside a header or footer.

refToShows
"default"Word’s “Paragraph number” (REF \r): the target’s number in context relative to where the reference sits. Levels it shares with the clause the reference is in (or, in an unnumbered paragraph, the numbered one before it) are left out: (ii) inside clause 2.3.1(a), 2.3.1(a)(ii) elsewhere. No trailing full stop, as in Word.
"noContext"Word’s “Paragraph number (no context)” (\n): the target’s own number as displayed, e.g. 3.1 or b).
"fullContext"Every level of the target’s number, joined with separator, e.g. 2.3.a.
"aboveBelow"“above” or “below”, in the target’s language.
"text"The target’s text, Word’s “Heading text” / “Paragraph text” (a Word REF with no number switch). Stored on the reference as content (bookmarkText), as inserted or imported, never turned into the number.
"page"The page the target is laid on, live from pagination (Word’s PAGEREF). pageLabel keeps the last known page for layouts without pages. Needs pagination to insert.

crossReferenceStore derives every other label from the target: renumbering, moving the target, editing an unnumbered target’s text or changing its language updates what the reference shows, without writing to the document (read it with getCrossReferenceEntry(node, state) or crossReferenceText(node, state)). A reference to a target without a number shows the target’s own text. Entries are keyed by the reference’s id, minted like a block’s.

If the target is deleted, the reference keeps its last label and is flagged missing, which renders it as broken. The label is written onto the reference once (missingLabel), in the transaction that removed the target, so it survives a reload and is undone with the removal; it is cleared when the target comes back. With no label to keep, the reference shows Word’s “Error! Reference source not found.”. Note numbers are derived the same way (noteAnchorStore, keyed by each anchor’s id).

Cross-references are allowed in note bodies. In a header or footer, the plugin replaces them with their label as plain text.

Editing

  • Insert. The References tab opens the cross-reference panel. Pick a target (numbered items or headings), a kind and, for fullContext, a separator. Then double-click the target or press Insert.
  • Edit. Right-click a selected reference and choose Edit. Inserting replaces the selected reference.
  • Preview. Hold Ctrl/⌘ with a reference selected, or use Open preview in the context menu.
  • Follow. Ctrl/Cmd+Click, or Enter on a focused reference, jumps to the target. This also works from inside a note, where it moves the main editor.

Commands

import { canInsertCrossReference, goToCrossRefTarget, insertCrossReference, } from "@stesura/references"; insertCrossReference({ targetId: "heading-node-id", refTo: "default", separator: null, })(state, dispatch);
CommandBehaviour
insertCrossReference(args)Replaces the selection with a reference and selects it. False if targetId or refTo is empty, if a text or page reference has no text (its bookmark text, or its page until pagination lays it out), or where an inline node can’t go (a selected table, a gap cursor). The other kinds ignore text: the store derives their label.
canInsertCrossReferenceThe position check alone, usable before a target is picked. The toolbar button probes it.
goToCrossRefTarget({ targetId, missingReference })Selects the target and scrolls to it. False for a missing target.

getCrossRef(state) / isCrossRefSelected(state) read the reference the selection covers exactly. computeFullContext and getAboveBelowText compute labels the same way the plugin does.

Next steps

Last updated on