diff --git a/src/documents/document/tests/point.rs b/src/documents/document/tests/point.rs index 79ecf8a..e759255 100644 --- a/src/documents/document/tests/point.rs +++ b/src/documents/document/tests/point.rs @@ -197,8 +197,9 @@ fn a_secondary_press_in_a_quote_lands_past_its_mark(cx: &mut TestAppContext) { let events = heard(&document, cx); let bounds = cx.debug_bounds("markdown-block-0").expect("drawn"); let (body, _) = sizes(cx); - let x = bounds.left() + px(31.0) + body * 0.6 * 7.25; - press_at(x, bounds.top() + px(10.0), MouseButton::Right, cx); + // "beta" draws on its own line. + let x = bounds.left() + px(31.0) + body * 0.6 * 1.25; + press_at(x, bounds.top() + px(10.0) + body * 1.6, MouseButton::Right, cx); let at = source.find("eta").unwrap(); assert_eq!(*events.borrow(), [target(at, TargetBlock::Quote)]); } diff --git a/src/documents/tree/blocks.rs b/src/documents/tree/blocks.rs index cc1a995..3644656 100644 --- a/src/documents/tree/blocks.rs +++ b/src/documents/tree/blocks.rs @@ -194,7 +194,7 @@ mod tests { let bold = 2..3; assert_eq!( (line.text.as_str(), &line.styles[..]), - ("a b c", &[(bold, Style::Strong)][..]) + ("a b\nc", &[(bold, Style::Strong)][..]) ); } diff --git a/src/documents/tree/html_block.rs b/src/documents/tree/html_block.rs index ff9245c..5f7aacd 100644 --- a/src/documents/tree/html_block.rs +++ b/src/documents/tree/html_block.rs @@ -197,7 +197,7 @@ mod tests { other => panic!("a paragraph, not {other:?}"), }) .collect(); - assert_eq!(lines, [""]); + assert_eq!(lines, [""]); let inline = html("a c").expect("text"); assert_eq!(inline.text, "a c"); assert_eq!(html("
\na < b { notes: HashMap, /// Every heading, as `[toc]` lists them. pub headings: Vec, + /// Soft breaks as lines, as the screen draws them. + lines: bool, } /// An open element, as HTML names it. @@ -122,6 +124,8 @@ struct Run { verbatim: bool, /// In a table cell, whose code reads `\|` as `|`. table: bool, + /// Soft breaks as lines, not spaces. + lines: bool, /// The current DOM text node's shortcodes. shortcodes: emoji::Shortcodes, /// White space owed, and the open elements before it. @@ -159,8 +163,9 @@ impl Run { } fn push_plain(&mut self, shown: &str) { - let soft = shown.replace("\r\n", " ").replace(['\r', '\n'], " "); - self.text.text.push_str(&soft); + let soft = if self.lines { "\n" } else { " " }; + let shown = shown.replace("\r\n", soft).replace(['\r', '\n'], soft); + self.text.text.push_str(&shown); } } @@ -175,8 +180,17 @@ pub fn parse(markdown: &str) -> Vec { } impl<'a> Reader<'a> { - /// A reader of `root`, parsed from `source`. + /// A reader of `root`, parsed from `source`, as the screen draws it. pub fn new(source: &'a str, root: &mdast::Node) -> Self { + Self::reading(source, root, true) + } + + /// The same, soft breaks joined as GFM output joins them. + pub fn joined(source: &'a str, root: &mdast::Node) -> Self { + Self::reading(source, root, false) + } + + fn reading(source: &'a str, root: &mdast::Node, lines: bool) -> Self { let mut nodes = Vec::new(); walk(root, &mut nodes); let (mut urls, mut notes, mut headings) = (HashMap::new(), HashMap::new(), Vec::new()); @@ -199,6 +213,7 @@ impl<'a> Reader<'a> { urls, notes, headings: Vec::new(), + lines: false, }; reader.headings = headings .into_iter() @@ -208,6 +223,8 @@ impl<'a> Reader<'a> { at, }) .collect(); + // Titles read on one line; the body as asked. + reader.lines = lines; reader } @@ -236,7 +253,7 @@ impl<'a> Reader<'a> { match node { mdast::Node::Paragraph(paragraph) => self.paragraph(node, ¶graph.children, out), mdast::Node::Heading(heading) => { - let (mut run, mut after) = (Run::default(), Vec::new()); + let (mut run, mut after) = (self.run(), Vec::new()); for child in &heading.children { self.inline_into(child, &mut run, &mut Images::After(&mut after)); } @@ -310,7 +327,7 @@ impl<'a> Reader<'a> { } let mut run = Run { table: matches!(node, mdast::Node::TableCell(_)), - ..Run::default() + ..self.run() }; for child in children { self.inline_into(child, &mut run, &mut Images::Split(out)); @@ -318,6 +335,13 @@ impl<'a> Reader<'a> { text_block(run.cut(), out); } + fn run(&self) -> Run { + Run { + lines: self.lines, + ..Run::default() + } + } + /// A footnote's number, by its first reference. pub fn note(&self, identifier: &str) -> Option { self.notes.get(identifier).copied() @@ -336,7 +360,7 @@ impl<'a> Reader<'a> { /// Inline text where images draw as their alt text. pub fn inline(&self, nodes: &[mdast::Node]) -> Inline { - let mut run = Run::default(); + let mut run = self.run(); for node in nodes { self.inline_into(node, &mut run, &mut Images::Alt); } diff --git a/src/documents/tree/tests.rs b/src/documents/tree/tests.rs index b8f867e..25896f6 100644 --- a/src/documents/tree/tests.rs +++ b/src/documents/tree/tests.rs @@ -68,20 +68,27 @@ fn an_image_splits_its_paragraph_and_references_resolve() { } #[test] -fn a_soft_break_reads_as_a_space_and_a_hard_one_as_a_line() { - for (source, shown) in [ - ("one\ntwo \nthree", "one two\nthree"), - ("one\r\ntwo \r\nthree", "one two\nthree"), - ("one\rtwo \rthree", "one two\nthree"), - ] { +fn a_soft_break_draws_as_a_line_and_joins_for_output() { + for source in ["one\ntwo \nthree", "one\r\ntwo \r\nthree", "one\rtwo \rthree"] { let nodes = parse(source); assert!( - matches!(&nodes[0], Node::Paragraph(text) if text.text == shown), - "{source:?} reads {nodes:?}" + matches!(&nodes[0], Node::Paragraph(text) if text.text == "one\ntwo\nthree"), + "{source:?} draws {nodes:?}" ); + let root = super::super::markdown::tree(source); + let paragraph = &root.children().expect("blocks")[0]; + let joined = Reader::joined(source, &root).inline(paragraph.children().expect("inlines")); + assert_eq!(joined.text, "one two\nthree", "{source:?} joins"); } } +#[test] +fn a_heading_title_reads_on_one_line() { + let source = "one\ntwo\n===\n"; + let root = super::super::markdown::tree(source); + assert_eq!(Reader::new(source, &root).headings[0].title, "one two"); +} + #[test] fn definitions_nested_in_quotes_and_lists_resolve() { for source in [ @@ -385,9 +392,9 @@ fn the_first_shortcode_decides_its_text_node() { ("a.:x.y: :smile:", "a.:x.y: šŸ˜„"), ("x\t:smile: y:smile:", "x\t:smile: y:smile:"), ("x\u{a0}:smile: y:smile:", "x\u{a0}:smile: y:smile:"), - ("x\n:smile: y:smile:", "x :smile: y:smile:"), + ("x\n:smile: y:smile:", "x\n:smile: y:smile:"), ("*x*:smile: :smile:", "xšŸ˜„ šŸ˜„"), - ("*x*\n:smile: y:smile:", "x šŸ˜„ yšŸ˜„"), + ("*x*\n:smile: y:smile:", "x\nšŸ˜„ yšŸ˜„"), ("x:smile: `c` :smile:", "x:smile: c šŸ˜„"), ("x:smile: :smile:", "x:smile: šŸ˜„"), ("x:smile:s :smile:", "x:smile:s šŸ˜„"),