Clean clipboard: copy as rich text (RTF + webarchive), paste HTML as Markdown#90
Merged
Conversation
Walk DocumentAST and emit a clean HTML fragment for the editor's rich-copy path, so selected raw markdown copies as proper rich text instead of the in-place-styled NSTextStorage junk (leaked syntax markers, raw caret line, `---` producing nothing). Test-first: 17 tests pin the output for headings, emphasis, code, links, lists, task lists, blockquotes, thematic breaks (the headline bug → <hr>), GFM tables, and HTML escaping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…wn as plain text Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…), keep ⌘⇧V raw Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…breaks, text escaping, li blocks, href Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…on real structure, drop dead table code Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align task-list content with bullet content (GitHub/Obsidian): the `[ ] ` chars collapse to the hidden-marker font so the hanging indent measures only `- ` and the box glyph is drawn in the bullet slot, right-aligned to the content. Shared TaskCheckboxGeometry keeps the fragment draw and click hit-test in sync; extend renderingSurfaceBounds so the box (left of the first glyph) isn't clipped; size from baseFont not NSTextView.font (whose getter returns the first char's font — 1px boxes in heading-first docs). Cursor: show the arrow over a checkbox box (skip super so it doesn't fight the I-beam and flicker), and drop the isEditable gate on the exclusion zone so a full-window overlay (search / transfer dialogs) fully owns the cursor instead of the suppressed read-only editor re-asserting one beneath it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… prose Chromium serializes a within-list selection as naked <li> elements without the ul/ol wrapper (Claude/ChatGPT copies), so the structure guard never fired and formatted lists pasted as plain text. Bare <li> now counts as structure and consecutive top-level items render as one tight bullet list (the ol numbering is lost with the wrapper — Obsidian parity). The guard also accepts formatted prose: two or more real <p> paragraphs with inline formatting convert, while a lone styled word/sentence and VS Code's div/span code stay on the plain-text path. Tag matching now checks a real boundary so prefixes can't false-match (<p vs <pre, <b vs <br>). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collapse the one-liner guard tests into two table-driven tests and merge the converter's granular fix tests into grouped ones (30 fewer test cases, ~150 fewer lines, every input still asserted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ckbox stand-ins Both rich flavors were derived from NSAttributedString(html:), whose importer silently drops <hr> and <input> — so the horizontal rule vanished from every rich paste target (the original review complaint) and task checkboxes decayed to naked bullets. The web archive now wraps our rendered html verbatim, and the RTF path substitutes visible stand-ins (─ rule, ☐/☑ glyphs) before converting. Blockquotes keep the plain rich-text convention (indentation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e RTF rule stand-in Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t unit The converter emitted two spaces per nesting level (GitHub source style). The editor renders indentation from the literal whitespace advance and its Tab key inserts \t (= one full 27.5pt indent step), so pasted sublists sat only ~7pt right of their parents. One tab per level makes a pasted nested list byte-identical to a typed one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on paste Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e anchor and full-document HTML paste; task items copy as plain bullets - Tests condensed 541→~360 added lines (25 wrappers merged/deleted; every user-validated behavior keeps exactly one pin). Dead CheckboxStyle config, an unreachable li case, and all temporary paste/table diagnostics removed; oversized comments shortened. - Fix: an edit touching any row of a table now restyles the WHOLE block — the image anchor sits on the block's first paragraph, and a scoped restyle clipped it away (pasted-adjacent tables stayed blank until a file switch). - Fix: full-document clipboard HTML (<html><head><body> wrappers, e.g. GPT) fell into the inline unwrap and glued all block text into one run; the wrappers are now transparent containers and head/style/script never leak. - Task items copy as plain list items in the rich flavors (user's call) — drops the checkbox-glyph rewrite machinery from the RTF path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… rich targets Dropping the checkbox from ALL flavors lost the task state when pasting into markdown apps (Obsidian converts the html flavor back to markdown). Split by audience: the .html flavor keeps <input type=checkbox> so - [ ]/- [x] survive into markdown targets; web archive and RTF strip it to the plain bullet the user prefers in rich targets. In-app paste was never affected (private raw flavor wins). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chat UIs (Claude) render task lists as literal "[ ] text" in plain <li>s; markdown escaping turned that into "\[ \] " which never re-renders as a checkbox. After an unordered item's head is final, reclaim the escaped prefix as a real "- [ ]" / "- [x]" marker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the editor's clipboard round-trip faithful in both directions. Previously copy serialized the in-place-styled raw-markdown storage (leaked syntax markers, first line raw,
---produced nothing in RTF) and paste dropped all rich structure to plain text.Copy (outgoing) —
33add33,505fac5The editor storage is raw markdown styled in place, so it isn't a real rich document. On copy we now render the selected markdown to a clean representation and put several flavors on the pasteboard:
MarkdownHTMLRendererwalksDocumentAST.parse→ clean HTML (----→<hr>, headings, bold/italic, inline+fenced code, links, ul/ol/task lists, blockquote with>stripped, GFM tables, all escaped).MarkdownPasteboardWriterbuilds anNSAttributedStringfrom that HTML and writes raw markdown (.string), RTF,com.apple.webarchive, and HTML.NativeTextView.copy(_:)override: empty selection falls back tosuper; otherwise the selected raw markdown goes to the writer.Result: rich targets (Notes / Mail / TextEdit-rich / Pages) get a faithful rendering — no raw first line,
----becomes a real rule — while plain-text targets still get the raw markdown.Paste (incoming) —
1262592,84f65c0Obsidian-style smart paste. Pasting rich content (Claude, browser, Word, Notion) now lands as Markdown:
HTMLToMarkdownConverter(lenient tag scanner + nesting stack):<ul><li>→-,<ol>→1./2., nested lists indented, GFM checkboxes, headings, bold/italic, links, inline+fenced code, blockquote,<hr>→----; unknown/style tags unwrapped; non-HTML input returns nil.paste(_:): HTML with structure converts to markdown (through the existingsanitizePastedText+ blockquote-continuation path); otherwise falls through to the unchanged plain-text handling. Image-embed and file-URL branches keep priority.pasteAsPlainText:and never routes through smart paste.Tests
30 new tests (17 renderer + 13 converter), all exact-output;
swift test= 195 tests / 29 suites green. Both directions were also exercised directly (pasteboard flavors on copy; a real Claude<ul>snippet →-bullets on paste).Notes / scope
$…$, wiki-links as their name, embedded images as<img src=localTarget>(may not survive cross-app paste). Follow-ups if wanted: syntax colors in copied code, richer image/LaTeX embedding.htmlTableToMarkdownhelper is now unused (tables go through the general converter); left in place as internal, harmless.🤖 Generated with Claude Code