diff --git a/apps/wordcraft/src/clipboard.rs b/apps/wordcraft/src/clipboard.rs index 7d1722e1c7..9946bfe601 100644 --- a/apps/wordcraft/src/clipboard.rs +++ b/apps/wordcraft/src/clipboard.rs @@ -128,6 +128,29 @@ pub fn is_own() -> bool { platform::is_own() } +/// Wrap HTML in the Windows CF_HTML header format required by clipboard readers. +/// +/// Offsets are byte offsets from the start of the payload; `EndHTML` excludes the trailing NUL. +#[cfg(any(target_os = "windows", test))] +fn wrap_cf_html(ctn: &str) -> Vec { + let h_version = "Version:0.9"; + let h_start_html = "\r\nStartHTML:"; + let h_end_html = "\r\nEndHTML:"; + let h_start_frag = "\r\nStartFragment:"; + let h_end_frag = "\r\nEndFragment:"; + let c_start_frag = "\r\n\r\n\r\n\r\n"; + let c_end_frag = "\r\n\r\n\r\n\0"; + let h_len = h_version.len() + h_start_html.len() + 10 + h_end_html.len() + 10 + h_start_frag.len() + 10 + h_end_frag.len() + 10; + let n_start_html = h_len + 2; + let n_start_frag = h_len + c_start_frag.len(); + let n_end_frag = n_start_frag + ctn.len(); + let n_end_html = n_end_frag + c_end_frag.len() - 1; + let header = format!( + "{h_version}{h_start_html}{n_start_html:010}{h_end_html}{n_end_html:010}{h_start_frag}{n_start_frag:010}{h_end_frag}{n_end_frag:010}{c_start_frag}{ctn}{c_end_frag}" + ); + header.into_bytes() +} + /// The clipboard's text. pub fn text() -> Option { open()?.get_text().ok() @@ -585,26 +608,6 @@ mod platform { false } - /// Wrap HTML in the Windows CF_HTML header format required by clipboard readers. - fn wrap_cf_html(ctn: &str) -> Vec { - let h_version = "Version:0.9"; - let h_start_html = "\r\nStartHTML:"; - let h_end_html = "\r\nEndHTML:"; - let h_start_frag = "\r\nStartFragment:"; - let h_end_frag = "\r\nEndFragment:"; - let c_start_frag = "\r\n\r\n\r\n\r\n"; - let c_end_frag = "\r\n\r\n\r\n\0"; - let h_len = h_version.len() + h_start_html.len() + 10 + h_end_html.len() + 10 + h_start_frag.len() + 10 + h_end_frag.len() + 10; - let n_start_html = h_len + 2; - let n_start_frag = h_len + c_start_frag.len(); - let n_end_frag = n_start_frag + ctn.len(); - let n_end_html = n_end_frag + c_end_frag.len() - 1; - let header = format!( - "{h_version}{h_start_html}{n_start_html:010}{h_end_html}{n_end_html:010}{h_start_frag}{n_start_frag:010}{h_end_frag}{n_end_frag:010}{c_start_frag}{ctn}{c_end_frag}" - ); - header.into_bytes() - } - /// Add PNG and CF_HTML formats alongside arboard's image, then mark ownership. pub fn after_set_image(image: &wordcraft_ui_egui::system_clipboard::CopyImage, html: &str, _text: &str) { if let Ok(png) = wordcraft_ui_egui::paste_picture::png_from_rgba(image.width, image.height, image.rgba.clone()) @@ -796,3 +799,33 @@ mod platform { /// Add MathML to the clipboard on the fallback platform. pub fn add_mathml(_mathml: &str) {} } + +#[cfg(test)] +mod cf_html_tests { + use super::wrap_cf_html; + + fn field(data: &[u8], name: &str) -> usize { + let at = data.windows(name.len()).position(|w| w == name.as_bytes()).expect("field") + name.len(); + std::str::from_utf8(&data[at..at + 10]).expect("offset digits").parse().expect("offset") + } + + #[test] + fn cf_html_offsets_delimit_the_fragment_bytes() { + let fragment = "

Olá, 世界 — café

"; + let data = wrap_cf_html(fragment); + + let start_html = field(&data, "StartHTML:"); + let end_html = field(&data, "EndHTML:"); + let start_fragment = field(&data, "StartFragment:"); + let end_fragment = field(&data, "EndFragment:"); + + assert!(start_html < start_fragment, "HTML starts before its fragment"); + assert!(start_fragment < end_fragment, "fragment has a positive length"); + assert!(end_fragment < end_html, "fragment ends inside the document"); + assert_eq!(&data[start_html..start_html + "".len()], b""); + assert_eq!(&data[start_fragment..end_fragment], fragment.as_bytes()); + assert_eq!(end_fragment - start_fragment, fragment.len(), "offsets count UTF-8 bytes"); + assert_eq!(&data[end_html..], b"\0", "EndHTML excludes the trailing NUL"); + assert_eq!(end_html, data.len() - 1); + } +} diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 35c64fbbbc..f878cc9691 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -365,9 +365,10 @@ impl ClipHistory { } pub(crate) fn push(&mut self, f: &Fragment, text: &str) { - // Text plus per-paragraph overhead; pictures are references into the document. + // Text plus per-paragraph overhead, plus the picture bytes the fragment keeps alive. let paras = text.matches('\n').count().saturating_add(f.blocks.len()).saturating_add(f.parts.len()).saturating_add(1); - let bytes = text.len().saturating_mul(2).saturating_add(paras.saturating_mul(256)); + let media = f.media.values().map(|m| m.len()).fold(0, usize::saturating_add); + let bytes = text.len().saturating_mul(2).saturating_add(paras.saturating_mul(256)).saturating_add(media); if bytes > CLIP_ITEM_BYTES || f.blocks.is_empty() { return; }