55 lines
6.8 KiB
Markdown
55 lines
6.8 KiB
Markdown
# 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 root `Cargo.toml`) has `Get::html()` (docs mirror https://doc.servo.org/arboard/struct.Get.html). Verify it compiles with `default-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.rs` reads 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`
|
|
```rust
|
|
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
|
|
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 `` 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.
|