Skip to content

Visual Editor silently drops empty attributed spans ([]{.class …}) on a Source↔Visual round-trip #1133

Description

@zinc75

Bug description

Originally reported downstream

When a document contains an empty attributed span[]{.class key="val"} with no text between the brackets — the Visual Editor deletes it on the first Source → Visual → Source round-trip. A non-empty span ([text]{.class …}) round-trips correctly. The deletion is silent (no warning), so it is easy to lose content.

Steps to reproduce

Start with this Markdown in Source mode:

Before []{.comment remark="EMPTY SPAN TEST"} after.

Before [some words]{.comment remark="NONEMPTY SPAN TEST"} after.

Switch to Visual mode, then back to Source mode, without making any edit. Result:

Before after.

Before [some words]{.comment remark="NONEMPTY SPAN TEST"} after.

The standalone empty span is gone; the one wrapping text remains.

Actual behavior

The empty span (and its attributes) is removed entirely.

This is not Pandoc — it's the editor's document model

Round-tripping the same input through the Pandoc that Quarto bundles preserves both spans, so nothing in Pandoc/Quarto strips it:

printf 'Before []{.comment remark="x"} after.\n' | quarto pandoc -f markdown -t markdown
# -> Before []{.comment remark="x"} after.        (empty span preserved)

The loss happens in panmirror (the Visual Editor's ProseMirror layer). A span is modeled as a ProseMirror mark, not a node — see packages/editor/src/marks/span.ts: the Pandoc Span token is read as a mark applied to the span's children (getChildren), and written back by re-wrapping those children.

A ProseMirror mark can only exist on text. An empty span has no children, so:

  • on read, there is nothing for the mark to attach to → the mark (and its attributes) is dropped;
  • on write-back, there is no marked text → no Span token is emitted.

This is consistent with the reader, which guards text creation with if (!text) return in pandoc_to_prosemirror.ts, and with prosemirror-model itself, where schema.text("") throws RangeError: Empty text nodes are not allowed — so an empty span can never produce an anchor node.

A non-empty span has children to carry the mark, which is exactly why it round-trips fine.

Impact

Any extension relying on empty attributed spans loses that content silently whenever the author touches the Visual Editor. Because it is a consequence of the "span = mark" modeling, no extension-side setting can recover it — it needs handling in the editor.

Expected behavior

Empty attributed spans are valid Pandoc AST and should survive the round-trip unchanged, exactly like non-empty ones. They are used by real extensions (e.g. annotation/comment tooling) to attach metadata at a point in the text, so dropping them is data loss.

Could you reserve empty attributed spans across the round-trip — e.g. by representing an empty span as a small node/anchor rather than a mark with no content, so its attributes survive ? Even preserving it as a raw/verbatim inline would avoid the silent data loss.

Your environment

The root cause is at the editor-model level (panmirror), so it is not expected to be platform- or IDE-specific (RStudio and the VS Code Quarto visual editor share panmirror).

Initial poster (@gasperploj) environment :

  • Quarto: 1.9.38
  • RStudio: 2026.07.0 Build 139
  • OS: Windows 10

Also reproduced by myself via the bundled Pandoc for the AST checks above on macOS / Quarto 1.10.18

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions