Skip to content

feat(editor): formatted paste; drop untyped pending formatting - #30

Merged
baseballyama merged 1 commit into
mainfrom
fix/editor-pending-format-rich-paste
Oct 6, 2026
Merged

baseballyama merged 1 commit into
mainfrom
fix/editor-pending-format-rich-paste

Conversation

@baseballyama

Copy link
Copy Markdown
Member

Summary

Pasting in the editor now keeps formatting and structure (bold/italic/underline/strike, super/subscript, color, headings, bullet and numbered lists, line breaks, tabs, tables). Formatting chosen at a caret and abandoned without typing no longer leaves an empty run behind.

Motivation

These were the two issues left open after #29. Until now, pasting from Word, a web page, or the editor itself dropped everything except plain text. Also, after pressing ⌘B at a caret and moving away without typing, an empty bold run stayed in the document. Each arrow key then stopped once on its invisible placeholder. Word handles both cases. There is no linked issue; the maintainer asked for these to be fixed.

Changes

  • parseClipboardHtml(html) (new, browser-only). Reads clipboard HTML into PastedBlocks.
    • Inline formatting comes from tags and inline styles. Google Docs' font-weight:normal wrapper correctly cancels <b>. Black is dropped, so the style's color still applies.
    • Headings h1–h6, and nested ul/ol levels.
    • Word's mso-list paragraphs: the level is read from the style and the kind from the marker. The typed marker itself is dropped.
    • Tables, cell by cell.
    • The editor's own copy: list labels, field-code characters and tab elements are handled.
    • HTML whitespace rules: only ASCII whitespace collapses, and &nbsp; becomes a space.
  • insertFragmentCommand (new). Pastes those blocks as one undo step, the way Word does:
    • the first pasted paragraph continues the caret's paragraph, and the rest of that paragraph follows the last pasted one;
    • text pasted within one paragraph keeps the destination's paragraph formatting;
    • consecutive list items of one kind share one list;
    • a table goes between the halves of the caret paragraph, with no empty paragraph before it when the caret was at the paragraph's start;
    • inside a cell, a pasted table becomes paragraphs;
    • with Track Changes on, the paste is recorded as tracked plain text.
  • releasePendingFormat(model, selection) (new). When the caret leaves the empty run that caret formatting created, without typing, the run is removed and later run indices are shifted. The canvas calls it on selection change and re-renders.
  • Canvas. Paste and text drops use the HTML when present, else plain text. Cut and paste sync the selection first, since a menu Cut or Paste comes without a keydown.
  • Internal. The list-id seeding is shared by applyListCommand and paste (commands/list-util.ts, not exported). The text-split loop is factored out of insertTextCommand.

Testing

  • New clipboard-html.test.ts (happy-dom), 13 tests.

    • Parser: tag and style formatting, the Google Docs wrapper, headings, nested lists, line breaks, whitespace, Word list paragraphs with markers, tables, and the editor's own copy.
    • Command: inline paste with undo, splitting around several paragraphs, list and heading formatting, table between halves (with save → reopen → validate), table at a paragraph start, table pasted into a cell, list on the first merged paragraph, no leftover empty runs.
  • text-selection.test.ts: pending run dropped when the caret moves, with indices shifted; pending run kept while the caret stays and once text is typed.

  • pnpm test: 1293 passed, 8 skipped. pnpm typecheck, pnpm -r run typecheck, svelte-check, check:api-page and check:tree-shake pass. oxlint reports the 2 known warnings.

  • Chromium, /editor:

    • a real ⌘C → ⌘V of a bold run and a bullet item;
    • a Word-style Japanese numbered list;
    • a table pasted into a cell;
    • plain-text paste;
    • ⌘B → arrow keys → the empty run is removed and the next arrow press moves.

    Random key fuzzing with pastes and caret formatting mixed in (3 seeds × 200 steps) found no DOM/model mismatch and no invalid package.

Breaking changes

None.

Checklist

  • I have read CLAUDE.md and followed the project's conventions.
  • I have added or updated tests for the change.
  • I have added or updated documentation where user-visible behavior changed.
  • If this is a breaking change, I have added a changeset / CHANGELOG entry and flagged it above.
  • I have re-read my own diff and removed dead code, debug prints, and stale comments.
  • If I used an LLM to draft this PR, I have verified each change myself, the PR represents real work that warrants a maintainer's review, and I am willing to defend each line in review.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UAiVj14HzKtRDqK12BXTr6

- Pasting HTML keeps character formatting, headings, lists, line breaks,
  tabs and tables (insertFragmentCommand + parseClipboardHtml), from Word,
  web pages, Google Docs and the editor's own copy. Text drops too.
- Formatting set at a caret and left without typing is dropped when the
  caret moves on (releasePendingFormat), instead of an empty run that
  swallowed an arrow-key press.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAiVj14HzKtRDqK12BXTr6
Copilot AI balanced review requested due to automatic review settings October 6, 2026 02:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@baseballyama
baseballyama merged commit 4dd27ca into main Oct 6, 2026
5 checks passed
@baseballyama
baseballyama deleted the fix/editor-pending-format-rich-paste branch October 6, 2026 02:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants