6.8 KiB
Clean paste: HTML from Word, Docs and browsers to GFM Markdown (agy gap 24, roadmap 29)
Part of specs/plugins-parity/. One new pure crate notescraft-htmlmd (L0, no third-party dependency); reused by link cards (link-cards/spec.md, OpenGraph scan).
Research (2026-10-21; agy claims checked)
- Obsidian ALREADY converts pasted rich text to Markdown natively (forum threads "Pasting now converts HTML and PDF markup to Markdown", a "Convert pasted HTML to Markdown" setting; some users want it optional). So agy's premise "Obsidian lacks this" is only half true: the gap is QUALITY and control, which the plugins sell: Paste Reformatter (detects HTML vs text, converts, optional cleanup, can override paste) https://community.obsidian.md/plugins/paste-reformatter , Smart Paste https://www.obsidianstats.com/plugins/smart-paste ; threads https://forum.obsidian.md/t/pasting-now-converts-html-and-pdf-markup-to-markdown/10506 and https://forum.obsidian.md/t/make-optional-turn-on-off-paste-that-converts-html-content-to-markdown-links/10096 . agy's source #2115 was not fetched.
- Bear, Notion, Apple Notes: they keep rich text natively; "sanitizes into Markdown" is not their model. Not verified; dropped.
- Feasibility:
arboard(already approved, version 3.6 in rootCargo.toml) hasGet::html()(docs mirror https://doc.servo.org/arboard/struct.Get.html). Verify it compiles withdefault-features = false, features = ["image-data"]in task 4 (STOP and report if it needs a feature flag: owner question Q1). - Current NotesCraft:
shell/attachment_drop/clipboard.rsreads only image and file lists; text paste is the egui/editor plain text. HTML is ignored.
Problem
Copying from Word, Google Docs or a web page pastes flat text (lists, links, bold lost) or, in other editors, a mess of inline styles. Users want clean semantic Markdown.
Goal
Ctrl/Cmd+V in the Markdown source/live editor, when the clipboard holds HTML (text/html), inserts converted GFM: headings, paragraphs, bold, italic, strike, inline code, code blocks, links, images as  (no download), nested lists, task checkboxes, blockquotes, simple tables, horizontal rules. Ctrl/Cmd+Shift+V pastes plain text (existing behaviour). A setting [editor] paste_html = "convert" | "plain" (default convert). Word/Docs junk is removed: <style>, <script>, <meta>, <o:p>, mso-* styles, conditional comments, Docs docs-internal-guid wrapper <b style="font-weight:normal">.
Non-goals (v1)
Downloading pasted images (a data: image is dropped with a toast "image skipped"; image bytes on the clipboard already follow the attachment path); PDF paste; Markdown to rich copy; colors, fonts and alignment (dropped on purpose); footnotes.
Design decisions
D1 Crate crates/htmlmd
pub struct Options { pub max_bytes: usize /*2 MiB*/, pub max_depth: usize /*64*/, pub max_output: usize /*4 MiB*/, pub keep_images: bool, pub base_url: Option<String> }
pub fn to_markdown(html: &str, opts: &Options) -> Result<Converted>; // Err(TooLarge) beyond limits, never truncates
pub struct Converted { pub markdown: String, pub dropped_images: usize }
pub mod tokens; // pub fn tokenize(html:&str, max_tokens:usize) -> Result<Vec<Token>> (also used by og meta scan)
Hand-written, forgiving tokenizer (tags, attributes with quoted/unquoted values, comments, <![CDATA[, entities & < ' 😀 ), no DOM crate. Unknown tags: children kept, tag dropped. javascript: / vbscript: / data: URLs in href/src are dropped (text kept). Output escapes Markdown specials only where they would change meaning at line start (#, -, +, 1., >) and inline * _ [ ] \ ` .
D2 Word and Docs quirks
Inline style heuristics: font-weight:700|bold -> strong, font-style:italic -> em, text-decoration:line-through -> strike (Docs wraps everything in spans); MsoListParagraph paragraphs with mso-list become list items (level from level1..n); <b> wrapper with font-weight:normal is transparent. Collapse whitespace like a browser; -> space except in code.
D3 UI and settings
Clipboard::Html { html, text } variant in shell/attachment_drop/clipboard.rs (read arboard::Clipboard::get().html() next to the text read, once per paste); Request::Text(String) (new) in shell/attachment_drop/mod.rs; shell/app/editor/attachments.rs::save_note_attachments inserts it through the same insert_command_result path (ONE undo step). Engine: edit.html_to_markdown {html} -> {markdown, dropped_images} command (READ in assistant_gate) in a new crates/engine/src/clipboard/ group so CLI/MCP can use it; the UI calls the library directly only if the command is not ready, no: the UI calls the command (everything is a command). Setting editor.paste_html in crates/engine/src/prefs/ (copy files_config), listed in prefs/catalog/. Strings [ui.paste] already exist (text/paste); add at the END of that table.
D4 Safety
Input is hostile (clipboard of any app): depth/size/token caps, no recursion beyond max_depth (iterative or bounded), no panics, no IO, no network, tag soup never loops. Output is plain text inserted into the buffer; no HTML survives.
Acceptance criteria
- A saved Word fixture (headings, bold, a 3-level list, a table) converts to the expected Markdown snapshot; a Google Docs fixture (span-styled bold/italic, links) likewise; a browser article fixture (nav, script, style dropped).
<script>alert(1)</script>never appears in output;javascript:links lose their URL.- A 3 MiB paste returns
TooLarge, the editor falls back to plain text with a toast. - Paste converts as ONE undo step; Shift+paste is plain; setting
plaindisables conversion. - Fuzz-ish test: 5 000 random tag soups from a fixed seed never panic and finish < 2 s.
- Round trip: converting the HTML rendered by
pulldown-cmarkfrom sample Markdown (headings, lists, code, table, links) gives back equivalent Markdown (normalised whitespace).
Open questions
- Q1:
arboardHTML getter needs no new dependency; confirm no feature flag is required. If one is, owner approval. - Q2: images with
http(s)src: keep as remotelinks (proposed, no fetch) or drop? - Q3: Linux Wayland returns HTML only through the clipboard owner; behaviour on failure is plain paste.
Feature ideas (researched, ranked)
- Paste URL over selection already exists (briefs/03); paste a bare URL with HTML title ->
[title](url)(Paste Reformatter does it). - Per-site cleanup rules (strip trailing "Read more", cookie banners) as a settings list of CSS-like selectors.
- "Paste as quote with source" adds
>and a source footer from the page<title>when the clipboard carriestext/x-moz-url. - Markdown tables from spreadsheet clipboards (Excel/Numbers
text/htmltables are the same path; add TSV fallback). - Link cards (roadmap 30) as an alternative for a bare URL paste.