collective

Browse / github.com/storytold/wordcraft / patch

Paste plain text that is Markdown as formatted text

Paste plain text that is Markdown as formatted text ## What Pasting text copied from a Markdown source (GitHub, Obsidian, an AI chat) now produces headings, lists, bold/italic, inline code, fenced code blocks, block quotes and GFM tables instead of literal `#` and `**`. Paste Special… (Ctrl+Alt+V) lists Markdown whenever the clipboard text parses as Markdown. ## Why People copy notes and answers out of Markdown tools all the time. WordCraft pasted the markers verbatim, so every paste had to be reformatted by hand — and "Keep Text Only" was the only alternative. Word has no Markdown paste, but this is the "goes further" the README promises, a

Status
open
Score
0.00
Adopters
0 other operators
Author
lucasmelo-mike01 (imported from pull request #4437, open)
Base
6a2b520fa49aa0d0595cea36589aaf16ca371cd3
Size
+360 −15 in 2 files
Review
pending
Shared
2026-10-28
Id
655d433b87289254e168d69b

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/paste.rs crates/engine/src/tests.rs

diff --git a/crates/engine/src/cmd/paste.rs b/crates/engine/src/cmd/paste.rs--- a/crates/engine/src/cmd/paste.rs+++ b/crates/engine/src/cmd/paste.rs@@ -1,12 +1,15 @@-//! Paste Special: paste the clipboard in a chosen format (formatted, RTF, HTML, picture,+//! Paste Special: paste the clipboard in a chosen format (formatted, Markdown, RTF, HTML, picture, //! unformatted), and the formatted content Paste and Merge Formatting take from other programs. //! //! The clipboard can hold several representations of the same content. WordCraft's own copy keeps //! a rich fragment; the system clipboard gives text, and callers (agents, the CLI, a front end-//! with a richer clipboard) may pass `html`, `rtf` or a `picture` too. Text that is itself RTF or-//! HTML source can also be pasted as that format. Paste keeps the source formatting of another-//! program's HTML or RTF; Merge Formatting keeps only its emphasis (#687).-//! Plain text has no formatting to keep, so Paste inserts it as Keep Text Only does.+//! with a richer clipboard) may pass `html`, `rtf` or a `picture` too. Text that is itself Markdown,+//! RTF or HTML source can also be pasted as that format. Paste keeps the source formatting of+//! another program's HTML or RTF; Merge Formatting keeps only its emphasis (#687).+//! Text with Markdown structure (headings, lists, tables, emphasis, code) reads as a document, as+//! opening a `.md` file does, so pasting it gives its headings and lists WordCraft's own+//! formatting instead of the source's punctuation; plain text has no formatting to keep, so Paste+//! inserts it as Keep Text Only does.  use std::collections::BTreeMap; use std::sync::Arc;@@ -26,8 +29,9 @@ pub const MAX_PICTURE_BYTES: usize = 96 << 20; const MAX_DEPTH: usize = 64;  /// The formats, in the order the dialog lists them: (id, label).-pub const FORMATS: [(&str, &str); 5] = [+pub const FORMATS: [(&str, &str); 6] = [     ("formatted", "Formatted Text"),+    ("markdown", "Markdown"),     ("rtf", "Rich Text Format (RTF)"),     ("html", "HTML Format"),     ("picture", "Picture"),@@ -35,11 +39,9 @@ pub const FORMATS: [(&str, &str); 5] = [ ];  pub fn specs() -> Vec<CommandSpec> {-    vec![-        CommandSpec::new("edit.pasteSpecial", "Paste Special", "Home › Clipboard › Paste", paste_special)-            .key("Mod+Alt+V")-            .params(r#"{"as"?: "formatted"|"rtf"|"html"|"picture"|"text", "text"?: string, "html"?: string, "rtf"?: string, "picture"?: base64}"#),-    ]+    vec![CommandSpec::new("edit.pasteSpecial", "Paste Special", "Home › Clipboard › Paste", paste_special).key("Mod+Alt+V").params(+        r#"{"as"?: "formatted"|"markdown"|"rtf"|"html"|"picture"|"text", "text"?: string, "html"?: string, "rtf"?: string, "picture"?: base64}"#,+    )] }  /// Clipboard text from the params (system clipboard), else WordCraft's own copy.@@ -59,6 +61,201 @@ fn looks_like_html(t: &str) -> bool {     t.starts_with('<') && (t.contains("</") || t.contains("/>")) } +/// How much of a pasted text [`looks_like_markdown`] reads, so a hostile clipboard can't make the+/// test long.+const MARKDOWN_SCAN_BYTES: usize = 64 << 10;+/// Most lines [`looks_like_markdown`] reads.+const MARKDOWN_SCAN_LINES: usize = 500;++/// The first `max` bytes of `t` (fewer if that would cut a character).+fn head_bytes(t: &str, max: usize) -> &str {+    if t.len() <= max {+        return t;+    }+    let mut end = max;+    while end > 0 && !t.is_char_boundary(end) {+        end -= 1;+    }+    t.get(..end).unwrap_or("")+}++/// A line without Markdown's indentation: up to three spaces. More than that, or a tab, starts an+/// indented code block, whose lines are code and not structure.+fn unindented(l: &str) -> &str {+    let mut s = l;+    for _ in 0..3 {+        match s.strip_prefix(' ') {+            Some(rest) => s = rest,+            None => break,+        }+    }+    s+}++/// The level of an ATX heading line (`## Tema`), if it is one.+fn atx_heading(l: &str) -> Option<u8> {+    let s = unindented(l);+    let n = s.chars().take_while(|&c| c == '#').count();+    if n == 0 || n > 6 {+        return None;+    }+    let rest = s.get(n..)?;+    (rest.is_empty() || rest.starts_with([' ', '\t'])).then_some(n as u8)+}++/// The fence character of a code fence line (```` ``` ```` or `~~~`), if it is one.+fn fence_marker(l: &str) -> Option<char> {+    let s = unindented(l);+    let c = s.chars().next()?;+    if c != '`' && c != '~' {+        return None;+    }+    (s.chars().take_while(|&x| x == c).count() >= 3).then_some(c)+}++/// Whether the line is a thematic break (`---`, `***`, `___`).+fn thematic_break(l: &str) -> bool {+    let s = unindented(l).trim_end_matches([' ', '\t']);+    let Some(c) = s.chars().next() else {+        return false;+    };+    if !matches!(c, '-' | '*' | '_') {+        return false;+    }+    s.chars().filter(|&x| x == c).count() >= 3 && s.chars().all(|x| x == c || x == ' ')+}++/// Whether the line is a block quote line (`> quoted`).+fn block_quote(l: &str) -> bool {+    let s = unindented(l);+    s == ">" || s.starts_with("> ") || s.starts_with(">\t")+}++/// Whether the line is a GFM table's delimiter row (`| --- | :-: |`).+fn table_delimiter(l: &str) -> bool {+    let s = unindented(l).trim();+    if !s.contains('|') {+        return false;+    }+    let cells: Vec<&str> = s.trim_matches('|').split('|').map(str::trim).collect();+    cells.len() >= 2+        && cells.iter().all(|c| {+            let c = c.strip_prefix(':').unwrap_or(c);+            let c = c.strip_suffix(':').unwrap_or(c);+            !c.is_empty() && c.chars().all(|x| x == '-')+        })+}++/// Whether the line is an unordered list item (`- a`).+fn bullet_item(l: &str) -> bool {+    let s = unindented(l);+    let Some(c) = s.chars().next() else {+        return false;+    };+    if !matches!(c, '-' | '*' | '+') {+        return false;+    }+    let rest = s.get(1..).unwrap_or("");+    matches!(rest.chars().next(), Some(' ') | Some('\t')) && !rest.trim().is_empty()+}++/// Whether the line is an ordered list item (`1. a`).+fn ordered_item(l: &str) -> bool {+    let s = unindented(l);+    let digits = s.chars().take_while(char::is_ascii_digit).count();+    if digits == 0 || digits > 9 {+        return false;+    }+    let rest = s.get(digits..).unwrap_or("");+    let mut it = rest.chars();+    if !matches!(it.next(), Some('.') | Some(')')) {+        return false;+    }+    matches!(it.next(), Some(' ') | Some('\t')) && !it.as_str().trim().is_empty()+}++/// Whether the line is a setext heading's underline (`===`).+fn setext_underline(l: &str) -> bool {+    let s = unindented(l).trim_end_matches([' ', '\t']);+    s.len() >= 2 && s.chars().all(|c| c == '=')+}++/// Emphasis, code and link markers in `t`, as pairs: `**bold**` is one, a link one, a code span+/// one.+fn inline_markers(t: &str) -> usize {+    let count = |m: &str| t.matches(m).count();+    let double = |m: &str| count(m) / 2;+    double("**") + double("__") + double("~~") + count("`") / 2 + count("](")+}++/// Whether clipboard text reads as a Markdown document rather than as prose: the punctuation+/// someone typed as text (`#`, `**`, `- `, `|`), which, pasted, becomes WordCraft's own headings,+/// lists, tables and emphasis, as opening a `.md` file does.+///+/// The test errs towards "no": one heading-looking line (a `#` comment in a shell snippet), one+/// `>` quote or one pair of `**` is text, and Keep Text Only pastes it raw. What it accepts: a+/// code fence, a GFM table, two headings, a heading beside emphasis, three list items, and so on.+/// Paste Special still offers Markdown and formats any text the user picks, whatever this says.+fn looks_like_markdown(t: &str) -> bool {+    if t.trim().is_empty() || t.len() > MAX_PASTE_BYTES {+        return false;+    }+    let head = head_bytes(t, MARKDOWN_SCAN_BYTES);+    let mut headings = 0usize;+    let mut fences = 0usize;+    let mut tables = 0usize;+    let mut quotes = 0usize;+    let mut rules = 0usize;+    let mut bullets = 0usize;+    let mut ordered = 0usize;+    let mut fence: Option<char> = None;+    for line in head.split('\n').take(MARKDOWN_SCAN_LINES) {+        if let Some(c) = fence {+            // Inside a code fence the lines are code: only the closing fence reads.+            if fence_marker(line) == Some(c) {+                fences += 1;+                fence = None;+            }+            continue;+        }+        if let Some(c) = fence_marker(line) {+            fences += 1;+            fence = Some(c);+        } else if atx_heading(line).is_some() {+            headings += 1;+        } else if table_delimiter(line) {+            tables += 1;+        } else if block_quote(line) {+            quotes += 1;+        } else if thematic_break(line) {+            rules += 1;+        } else if bullet_item(line) {+            bullets += 1;+        } else if ordered_item(line) {+            ordered += 1;+        } else if setext_underline(line) {+            headings += 1;+        }+    }+    let structural = headings + fences + tables + quotes + rules;+    let lists = bullets + ordered;+    let inline = inline_markers(head);+    // A code block, a table or a second heading is Markdown whatever else the text holds.+    if fences >= 2 || tables > 0 || headings >= 2 {+        return true;+    }+    // A heading over emphasized text (`## Tema` and `**nota**`) is a document.+    if headings > 0 && inline >= 2 {+        return true;+    }+    // Three list items are a list, not three lines that happen to begin with a dash.+    if bullets >= 3 {+        return true;+    }+    // Anything else that adds up: a quote and two items, a rule and bold and a link, …+    structural > 0 && structural + lists + inline >= 3+}+ /// The source for a format, if the clipboard has it. fn source(s: &Session, v: &Value, fmt: &str) -> Option<String> {     let non_empty = |t: String| if t.is_empty() { None } else { Some(t) };@@ -73,6 +270,9 @@ fn source(s: &Session, v: &Value, fmt: &str) -> Option<String> {         }         "rtf" => p::str(v, "rtf").map(str::to_string).or_else(|| clip_text(s, v).filter(|t| looks_like_rtf(t))).and_then(non_empty),         "html" => p::str(v, "html").map(str::to_string).or_else(|| clip_text(s, v).filter(|t| looks_like_html(t))).and_then(non_empty),+        // Picking Markdown formats any text, as picking HTML parses HTML source; only the dialog's+        // list of formats (see `available`) is what the test keeps off text that isn't Markdown.+        "markdown" => clip_text(s, v).filter(|t| !t.trim().is_empty() && t.len() <= MAX_PASTE_BYTES),         "picture" => p::str(v, "picture").map(str::to_string).and_then(non_empty),         "text" => clip_text(s, v).and_then(non_empty),         _ => None,@@ -81,7 +281,16 @@ fn source(s: &Session, v: &Value, fmt: &str) -> Option<String> {  /// Formats the clipboard can be pasted as, best first. pub fn available(s: &Session, v: &Value) -> Vec<&'static str> {-    FORMATS.iter().map(|(id, _)| *id).filter(|id| source(s, v, id).is_some()).collect()+    FORMATS+        .iter()+        .map(|(id, _)| *id)+        .filter(|id| match *id {+            // Offered for text that reads as Markdown, not for every text: plain prose is what Keep+            // Text Only is for, and the dialog shouldn't suggest turning it into a document.+            "markdown" => clip_text(s, v).is_some_and(|t| looks_like_markdown(&t)),+            _ => source(s, v, id).is_some(),+        })+        .collect() }  fn formats_json(ids: &[&str]) -> Value {@@ -113,11 +322,14 @@ fn paste_special(s: &mut Session, v: &Value) -> CmdResult {     };     let fmt = match fmt.to_ascii_lowercase().as_str() {         "formatted" | "keep" | "wordcraft" => "formatted",+        "markdown" | "md" => "markdown",         "rtf" => "rtf",         "html" => "html",         "picture" | "png" | "image" => "picture",         "text" | "unformatted" | "plain" => "text",-        other => return Err(CmdError::Params(format!("unknown format `{other}` (formatted, rtf, html, picture or text)"))),+        other => {+            return Err(CmdError::Params(format!("unknown format `{other}` (formatted, markdown, rtf, html, picture or text)")));+        }     };     let Some(src) = source(s, v, fmt) else {         return Err(CmdError::Failed(if ids.is_empty() {@@ -139,6 +351,7 @@ fn paste_special(s: &mut Session, v: &Value) -> CmdResult {             return Ok(r);         }         "formatted" => s.clipboard.clone().ok_or_else(|| CmdError::Failed("the clipboard is empty".into()))?,+        "markdown" => adopt(s, wordcraft_formats::markdown::import(&src)),         "html" => adopt(s, import_html(&src)),         _ => adopt(s, wordcraft_formats::rtf::import(src.as_bytes()).map_err(|e| CmdError::Failed(format!("bad RTF: {e}")))?),     };@@ -259,14 +472,16 @@ pub fn rich_fragment(s: &mut Session, v: &Value) -> Option<Fragment> {  /// What a Paste brings in. pub enum Pasted {-    /// WordCraft's own copy, or another program's HTML or RTF: it keeps the formatting it carries.+    /// WordCraft's own copy, or another program's HTML, RTF or Markdown: it keeps the formatting+    /// it carries.     Fragment(Fragment),     /// The system clipboard's text, lines separated by `\n`.     Text(String), }  /// What Paste inserts: WordCraft's own copy while the system clipboard still holds it, else-/// another program's formatted copy (HTML, RTF), else the system clipboard's text.+/// another program's formatted copy (HTML, RTF), else Markdown that reads as a document, else the+/// system clipboard's text. pub fn paste_content(s: &mut Session, v: &Value) -> Result<Pasted, CmdError> {     let text = p::str(v, "text").map(|t| t.replace("\r\n", "\n"));     if let Some(f) = &s.clipboard@@ -278,6 +493,15 @@ pub fn paste_content(s: &mut Session, v: &Value) -> Result<Pasted, CmdError> {     if let Some(f) = rich_fragment(s, v) {         return Ok(Pasted::Fragment(f));     }+    if let Some(t) = text.as_deref().filter(|t| looks_like_markdown(t)) {+        // Markdown that reads as a document: its headings, lists, tables and emphasis become this+        // document's formatting, as opening a `.md` file does. Keep Text Only pastes it as text,+        // and so does Paste Special › Unformatted Text.+        let doc = wordcraft_formats::markdown::import(t);+        if !doc.body.is_empty() {+            return Ok(Pasted::Fragment(adopt(s, doc)));+        }+    }     match text {         Some(t) if !t.is_empty() => Ok(Pasted::Text(t)),         _ => Err(CmdError::Failed("the clipboard is empty".into())),diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs--- a/crates/engine/src/tests.rs+++ b/crates/engine/src/tests.rs@@ -1673,6 +1673,127 @@ fn paste_keeps_source_formatting_from_other_programs() {     assert_eq!(props_at(&s, 4).italic, Some(true)); } +/// Markdown copied as text reads as a document when pasted: its headings, lists, tables and+/// emphasis become WordCraft's own formatting, as opening a `.md` file does — instead of the+/// source's `##`, `**` and `- ` landing in the text.+#[test]+fn paste_reads_markdown_as_a_formatted_document() {+    let mut s = s();+    let md =+        "# Título\n\n## Tema central da questão\n\n**Direito** e *ênfase*\n\n- A\n- B\n\n> citação\n\n---\n\n| A | B |\n| :-: | --- |\n| 1 | 2 |\n";+    run(&mut s, "edit.paste", json!({"text": md}));+    let plain = text(&s);+    assert!(plain.starts_with("Título\nTema central da questão\nDireito e ênfase"), "{plain:?}");+    for marker in ["#", "**", "*ênfase*", "- A", "| ---"] {+        assert!(!plain.contains(marker), "{marker:?} left in {plain:?}");+    }+    let style = |s: &Session, start: &str| {+        s.doc.body.iter().filter_map(|b| b.as_para()).find(|p| p.text.starts_with(start)).and_then(|p| p.props.style.clone())+    };+    assert_eq!(style(&s, "Título").as_deref(), Some("Heading1"));+    assert_eq!(style(&s, "Tema central").as_deref(), Some("Heading2"));+    assert_eq!(style(&s, "citação").as_deref(), Some("Quote"));+    // Emphasis: bold on "Direito", italic on "ênfase".+    let at = s.doc.body.iter().position(|b| b.as_para().is_some_and(|p| p.text.starts_with("Direito"))).unwrap();+    assert!(bold_at(&s, at, 0));+    assert_eq!(s.doc.para_at(&Pos::body(at, 10)).unwrap().props_of_char(10).italic, Some(true));+    // A bullet list, with its numbering definition adopted.+    let lists: Vec<_> = s.doc.body.iter().filter_map(|b| b.as_para()).filter(|p| p.props.numbering.is_some_and(|n| n.num != 0)).collect();+    assert_eq!(lists.len(), 2, "{plain:?}");+    assert!(lists.iter().all(|p| s.doc.numbering.nums.iter().any(|n| n.id == p.props.numbering.unwrap().num)));+    // The pipe table is a table.+    assert!(s.doc.body.iter().any(|b| matches!(&**b, wordcraft_doc::Block::Table(_))), "{plain:?}");+}++/// Paste Special offers Markdown for text that reads as Markdown, formats that text (and any text+/// the user picks Markdown for), and doesn't offer it for prose.+#[test]+fn paste_special_lists_and_pastes_markdown() {+    let mut s = s();+    let md = "## Tema\n\n**negrito** e lista:\n\n- um\n- dois\n- três\n";+    let r = run(&mut s, "edit.pasteSpecial", json!({"text": md}));+    let ids: Vec<&str> = r["formats"].as_array().unwrap().iter().map(|f| f["id"].as_str().unwrap()).collect();+    assert_eq!(ids, ["markdown", "text"]);+    let r = run(&mut s, "edit.pasteSpecial", json!({"as": "markdown", "text": md}));+    assert_eq!(r["pastedAs"], "markdown");+    assert!(text(&s).starts_with("Tema\nnegrito e lista:\num\ndois\ntrês"), "{}", text(&s));+    assert_eq!(s.doc.body.first().and_then(|b| b.as_para()).and_then(|p| p.props.style.as_deref()), Some("Heading2"));+    // Picking Markdown formats text the detection would leave alone: one pair of `**` is enough+    // once the user says what the text is.+    let mut t = self::s();+    run(&mut t, "edit.pasteSpecial", json!({"as": "markdown", "text": "usa **negrito** aqui"}));+    assert_eq!(text(&t), "usa negrito aqui");+    assert!(bold_at(&t, 0, 4));+    // Prose: Markdown is not offered.+    let r = run(&mut t, "edit.pasteSpecial", json!({"text": "Art. 1º O Município é pessoa jurídica de direito público interno."}));+    let ids: Vec<&str> = r["formats"].as_array().unwrap().iter().map(|f| f["id"].as_str().unwrap()).collect();+    assert_eq!(ids, ["text"]);+}++/// Text that only looks a little like Markdown stays text: one `#` comment line, one `>` quote or+/// one pair of `**` is prose, not a document, and so are numbered lines with no other structure.+#[test]+fn markdown_detection_keeps_prose_as_text() {+    let offered = |sample: &str| {+        let mut s = s();+        let r = run(&mut s, "edit.pasteSpecial", json!({"text": sample}));+        r["formats"].as_array().unwrap().iter().any(|f| f["id"] == "markdown")+    };+    for sample in ["# git status\ngit status --short", "Use **cuidado** ao citar o art. 5º.", "> ver nota anterior", "1. Art. 5º\n2. Art. 6º"] {+        assert!(!offered(sample), "read as Markdown: {sample:?}");+        let mut s = s();+        run(&mut s, "edit.paste", json!({"text": sample}));+        assert_eq!(text(&s), sample, "pasted as text: {sample:?}");+        assert!(s.doc.body.iter().filter_map(|b| b.as_para()).all(|p| p.props.style.is_none() && p.props.numbering.is_none()), "{sample:?}");+    }+    for sample in ["# Um\n\n# Dois", "```\ncode\n```", "| A | B |\n| - | - |\n| 1 | 2 |", "- a\n- b\n- c", "## Tema\n\n**negrito** e `código`"] {+        assert!(offered(sample), "not read as Markdown: {sample:?}");+    }+}++/// Keep Text Only keeps the Markdown source, whatever Paste would make of it.+#[test]+fn paste_text_keeps_the_markdown_source() {+    let mut s = s();+    let md = "## Tema\n\n**negrito**\n\n- um\n- dois\n- três\n";+    run(&mut s, "edit.pasteText", json!({"text": md}));+    assert_eq!(text(&s), md);+    assert!(s.doc.body.iter().filter_map(|b| b.as_para()).all(|p| p.props.style.is_none() && p.props.numbering.is_none()), "{}", text(&s));+    // A statute pasted as text: numbered lines are its text, not a list to renumber.+    let law = "Art. 1º O Município é pessoa jurídica de direito público interno.\n\n1. A autonomia política;\n2. A autonomia financeira;\n\n* Ver a Lei Orgânica.";+    let mut t = self::s();+    run(&mut t, "edit.paste", json!({"text": law}));+    assert_eq!(text(&t), law);+    assert!(t.doc.body.iter().filter_map(|b| b.as_para()).all(|p| p.props.numbering.is_none()));+}++/// Headings pasted from Markdown keep their styles, outline levels and emoji through a save and+/// a reopen: the Navigator derives its tree from those levels, so the outline survives the file.+#[test]+fn pasted_headings_keep_their_outline_and_emoji_after_saving() {+    let mut s = s();+    run(&mut s, "edit.paste", json!({"text": "## 1️⃣ Tema central\n\n### 📘 Conceito Básico\n\n## 2️⃣ Resumo teórico\n"}));+    assert_eq!(text(&s), "1️⃣ Tema central\n📘 Conceito Básico\n2️⃣ Resumo teórico");+    let dir = std::env::temp_dir().join(format!("wc-outline-{}", std::process::id()));+    std::fs::create_dir_all(&dir).unwrap();+    let path = dir.join("outline.docx");+    run(&mut s, "file.saveAs", json!({"path": path.to_str().unwrap()}));+    let mut t = self::s();+    run(&mut t, "file.open", json!({"path": path.to_str().unwrap()}));+    let outline: Vec<Option<u8>> = t+        .doc+        .body+        .iter()+        .filter_map(|b| b.as_para())+        .filter(|p| p.text.trim() != "")+        .map(|p| t.doc.styles.resolve_para(&p.props).outline_level)+        .collect();+    assert_eq!(outline, [Some(1), Some(2), Some(1)], "Heading 2, Heading 3, Heading 2");+    assert!(text(&t).contains("1\u{FE0F}\u{20E3}"), "the keycap code points survive the file: {:?}", text(&t));+    assert!(text(&t).contains("📘"), "the marker emoji survives the file: {:?}", text(&t));+    let _ = std::fs::remove_dir_all(&dir);+}+ /// A CF_HTML ("HTML Format") clipboard payload around `html`, with the byte offsets Windows writes. fn cf_html(html: &str) -> String {     let n = "Version:1.0\r\nStartHTML:0000000000\r\nEndHTML:0000000000\r\nStartFragment:0000000000\r\nEndFragment:0000000000\r\n".len();