Files
Marcos Paulo b32650767c
ci / ci (macos-latest) (push) Has been cancelled
ci / ci (ubuntu-latest) (push) Has been cancelled
ci / ci (windows-latest) (push) Has been cancelled
docs: specs and briefs for smart folders, version diff, media timestamps, clean paste, link cards, spaced repetition
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 12:18:00 -03:00

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)

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 ![alt](url) (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 &amp; &lt; &#39; &#x1F600; &nbsp;), 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; &nbsp; -> 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

  1. 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).
  2. <script>alert(1)</script> never appears in output; javascript: links lose their URL.
  3. A 3 MiB paste returns TooLarge, the editor falls back to plain text with a toast.
  4. Paste converts as ONE undo step; Shift+paste is plain; setting plain disables conversion.
  5. Fuzz-ish test: 5 000 random tag soups from a fixed seed never panic and finish < 2 s.
  6. Round trip: converting the HTML rendered by pulldown-cmark from sample Markdown (headings, lists, code, table, links) gives back equivalent Markdown (normalised whitespace).

Open questions

  • Q1: arboard HTML getter needs no new dependency; confirm no feature flag is required. If one is, owner approval.
  • Q2: images with http(s) src: keep as remote ![](url) links (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)

  1. Paste URL over selection already exists (briefs/03); paste a bare URL with HTML title -> [title](url) (Paste Reformatter does it).
  2. Per-site cleanup rules (strip trailing "Read more", cookie banners) as a settings list of CSS-like selectors.
  3. "Paste as quote with source" adds > and a source footer from the page <title> when the clipboard carries text/x-moz-url.
  4. Markdown tables from spreadsheet clipboards (Excel/Numbers text/html tables are the same path; add TSV fallback).
  5. Link cards (roadmap 30) as an alternative for a bare URL paste.