Paste

Clipboard content becomes a planned intent.

Paste is where editors often lose determinism. HTML arrives from arbitrary sources, styles mismatch, and the editor starts pattern-matching rendered structure instead of preserving semantic evidence.

SpineEditor keeps paste on the semantic pipeline. The substrate reads the clipboard, builds a PastePayload, and dispatches a PasteIntent. The planner converts the payload into a Fragment, decides whether the supported domain is structural or inline, and lowers the result through a transaction.

Worked example

Plain text paste:

engine.dispatch({
  kind: "paste",
  payload: {
    kind: "text",
    text: "line one\nline two",
  },
});

Fragment paste (from an internal copy or a structured source):

engine.dispatch({
  kind: "paste",
  payload: {
    kind: "fragment",
    fragment: copiedFragment,
  },
});

Both trigger the same trace sequence as any other intent:

intentReceived
  intentKind: "paste"

planCompiled
  intentKind: "paste"
  opCount: ...

transactionApplied
  stepCount: ...

history
  action: "push"

If schema repair changes the applied document, one or more normalized events appear before history.

Payload shape

type PastePayload = PlainTextPastePayload | FragmentPastePayload;

interface PlainTextPastePayload {
  readonly kind: "text";
  readonly text: string;
}

interface FragmentPastePayload {
  readonly kind: "fragment";
  readonly fragment: Fragment;
}

Helpers:

  • splitPlainTextLines(text) — split on newlines with deterministic handling of trailing empties.
  • plainTextToFragment(text, options?) — wrap lines into paragraph blocks.
  • pastePayloadToFragment(payload) — unify the two cases.
  • isEmptyPastePayload(payload) — cheap short-circuit for no-op pastes.
  • pastePayloadToJSON(payload) — canonical serialization for trace/replay.

Structural vs inline

The paste planner (packages/core/src/plan/plan-paste-structural.ts and siblings) picks between two strategies:

  • Structural paste. The incoming fragment contains supported block-level content (paragraphs, lists, media). On the earned domains, the planner inserts or replaces structure without flattening it to plain text.
  • Inline paste. The incoming fragment is text-only and merges into the current text run. Marks from the paste site are preserved or stripped by the planner rule.

The choice is deterministic. Unsupported structural domains fail closed into the explicit fallback instead of silently inventing semantics.