Documents: soft breaks draw as lines; Reader::joined keeps GFM spaces
The screen keeps a single line break (Typora's default when writing); outputs read with Reader::joined, which joins them as GFM does. Heading titles read on one line. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a2cb531e06
commit
93efa7f70b
5 files changed
+52
-20
No files matched your search
@@ -197,8 +197,9 @@ fn a_secondary_press_in_a_quote_lands_past_its_mark(cx: &mut TestAppContext) {
|
|||||||
let events = heard(&document, cx);
|
let events = heard(&document, cx);
|
||||||
let bounds = cx.debug_bounds("markdown-block-0").expect("drawn");
|
let bounds = cx.debug_bounds("markdown-block-0").expect("drawn");
|
||||||
let (body, _) = sizes(cx);
|
let (body, _) = sizes(cx);
|
||||||
let x = bounds.left() + px(31.0) + body * 0.6 * 7.25;
|
// "beta" draws on its own line.
|
||||||
press_at(x, bounds.top() + px(10.0), MouseButton::Right, cx);
|
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();
|
let at = source.find("eta").unwrap();
|
||||||
assert_eq!(*events.borrow(), [target(at, TargetBlock::Quote)]);
|
assert_eq!(*events.borrow(), [target(at, TargetBlock::Quote)]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ mod tests {
|
|||||||
let bold = 2..3;
|
let bold = 2..3;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
(line.text.as_str(), &line.styles[..]),
|
(line.text.as_str(), &line.styles[..]),
|
||||||
("a b c", &[(bold, Style::Strong)][..])
|
("a b\nc", &[(bold, Style::Strong)][..])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ mod tests {
|
|||||||
other => panic!("a paragraph, not {other:?}"),
|
other => panic!("a paragraph, not {other:?}"),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(lines, ["<script> first", "second </script>"]);
|
assert_eq!(lines, ["<script> first", "second\n</script>"]);
|
||||||
let inline = html("a <script>b</script> c").expect("text");
|
let inline = html("a <script>b</script> c").expect("text");
|
||||||
assert_eq!(inline.text, "a <script>b</script> c");
|
assert_eq!(inline.text, "a <script>b</script> c");
|
||||||
assert_eq!(html("<div>\na < b <i").expect("text").text, "a < b");
|
assert_eq!(html("<div>\na < b <i").expect("text").text, "a < b");
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ pub struct Reader<'a> {
|
|||||||
notes: HashMap<String, usize>,
|
notes: HashMap<String, usize>,
|
||||||
/// Every heading, as `[toc]` lists them.
|
/// Every heading, as `[toc]` lists them.
|
||||||
pub headings: Vec<DocumentHeading>,
|
pub headings: Vec<DocumentHeading>,
|
||||||
|
/// Soft breaks as lines, as the screen draws them.
|
||||||
|
lines: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An open element, as HTML names it.
|
/// An open element, as HTML names it.
|
||||||
@@ -122,6 +124,8 @@ struct Run {
|
|||||||
verbatim: bool,
|
verbatim: bool,
|
||||||
/// In a table cell, whose code reads `\|` as `|`.
|
/// In a table cell, whose code reads `\|` as `|`.
|
||||||
table: bool,
|
table: bool,
|
||||||
|
/// Soft breaks as lines, not spaces.
|
||||||
|
lines: bool,
|
||||||
/// The current DOM text node's shortcodes.
|
/// The current DOM text node's shortcodes.
|
||||||
shortcodes: emoji::Shortcodes,
|
shortcodes: emoji::Shortcodes,
|
||||||
/// White space owed, and the open elements before it.
|
/// White space owed, and the open elements before it.
|
||||||
@@ -159,8 +163,9 @@ impl Run {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn push_plain(&mut self, shown: &str) {
|
fn push_plain(&mut self, shown: &str) {
|
||||||
let soft = shown.replace("\r\n", " ").replace(['\r', '\n'], " ");
|
let soft = if self.lines { "\n" } else { " " };
|
||||||
self.text.text.push_str(&soft);
|
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<Node> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Reader<'a> {
|
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 {
|
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();
|
let mut nodes = Vec::new();
|
||||||
walk(root, &mut nodes);
|
walk(root, &mut nodes);
|
||||||
let (mut urls, mut notes, mut headings) = (HashMap::new(), HashMap::new(), Vec::new());
|
let (mut urls, mut notes, mut headings) = (HashMap::new(), HashMap::new(), Vec::new());
|
||||||
@@ -199,6 +213,7 @@ impl<'a> Reader<'a> {
|
|||||||
urls,
|
urls,
|
||||||
notes,
|
notes,
|
||||||
headings: Vec::new(),
|
headings: Vec::new(),
|
||||||
|
lines: false,
|
||||||
};
|
};
|
||||||
reader.headings = headings
|
reader.headings = headings
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -208,6 +223,8 @@ impl<'a> Reader<'a> {
|
|||||||
at,
|
at,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
// Titles read on one line; the body as asked.
|
||||||
|
reader.lines = lines;
|
||||||
reader
|
reader
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +253,7 @@ impl<'a> Reader<'a> {
|
|||||||
match node {
|
match node {
|
||||||
mdast::Node::Paragraph(paragraph) => self.paragraph(node, ¶graph.children, out),
|
mdast::Node::Paragraph(paragraph) => self.paragraph(node, ¶graph.children, out),
|
||||||
mdast::Node::Heading(heading) => {
|
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 {
|
for child in &heading.children {
|
||||||
self.inline_into(child, &mut run, &mut Images::After(&mut after));
|
self.inline_into(child, &mut run, &mut Images::After(&mut after));
|
||||||
}
|
}
|
||||||
@@ -310,7 +327,7 @@ impl<'a> Reader<'a> {
|
|||||||
}
|
}
|
||||||
let mut run = Run {
|
let mut run = Run {
|
||||||
table: matches!(node, mdast::Node::TableCell(_)),
|
table: matches!(node, mdast::Node::TableCell(_)),
|
||||||
..Run::default()
|
..self.run()
|
||||||
};
|
};
|
||||||
for child in children {
|
for child in children {
|
||||||
self.inline_into(child, &mut run, &mut Images::Split(out));
|
self.inline_into(child, &mut run, &mut Images::Split(out));
|
||||||
@@ -318,6 +335,13 @@ impl<'a> Reader<'a> {
|
|||||||
text_block(run.cut(), out);
|
text_block(run.cut(), out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run(&self) -> Run {
|
||||||
|
Run {
|
||||||
|
lines: self.lines,
|
||||||
|
..Run::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A footnote's number, by its first reference.
|
/// A footnote's number, by its first reference.
|
||||||
pub fn note(&self, identifier: &str) -> Option<usize> {
|
pub fn note(&self, identifier: &str) -> Option<usize> {
|
||||||
self.notes.get(identifier).copied()
|
self.notes.get(identifier).copied()
|
||||||
@@ -336,7 +360,7 @@ impl<'a> Reader<'a> {
|
|||||||
|
|
||||||
/// Inline text where images draw as their alt text.
|
/// Inline text where images draw as their alt text.
|
||||||
pub fn inline(&self, nodes: &[mdast::Node]) -> Inline {
|
pub fn inline(&self, nodes: &[mdast::Node]) -> Inline {
|
||||||
let mut run = Run::default();
|
let mut run = self.run();
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
self.inline_into(node, &mut run, &mut Images::Alt);
|
self.inline_into(node, &mut run, &mut Images::Alt);
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-10
@@ -68,20 +68,27 @@ fn an_image_splits_its_paragraph_and_references_resolve() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_soft_break_reads_as_a_space_and_a_hard_one_as_a_line() {
|
fn a_soft_break_draws_as_a_line_and_joins_for_output() {
|
||||||
for (source, shown) in [
|
for source in ["one\ntwo \nthree", "one\r\ntwo \r\nthree", "one\rtwo \rthree"] {
|
||||||
("one\ntwo \nthree", "one two\nthree"),
|
|
||||||
("one\r\ntwo \r\nthree", "one two\nthree"),
|
|
||||||
("one\rtwo \rthree", "one two\nthree"),
|
|
||||||
] {
|
|
||||||
let nodes = parse(source);
|
let nodes = parse(source);
|
||||||
assert!(
|
assert!(
|
||||||
matches!(&nodes[0], Node::Paragraph(text) if text.text == shown),
|
matches!(&nodes[0], Node::Paragraph(text) if text.text == "one\ntwo\nthree"),
|
||||||
"{source:?} reads {nodes:?}"
|
"{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]
|
#[test]
|
||||||
fn definitions_nested_in_quotes_and_lists_resolve() {
|
fn definitions_nested_in_quotes_and_lists_resolve() {
|
||||||
for source in [
|
for source in [
|
||||||
@@ -385,9 +392,9 @@ fn the_first_shortcode_decides_its_text_node() {
|
|||||||
("a.:x.y: :smile:", "a.:x.y: 😄"),
|
("a.:x.y: :smile:", "a.:x.y: 😄"),
|
||||||
("x\t:smile: y:smile:", "x\t:smile: y:smile:"),
|
("x\t:smile: y:smile:", "x\t:smile: y:smile:"),
|
||||||
("x\u{a0}:smile: y:smile:", "x\u{a0}: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*: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: `c` :smile:", "x:smile: c 😄"),
|
||||||
("x:smile:<!-- c --> :smile:", "x:smile: 😄"),
|
("x:smile:<!-- c --> :smile:", "x:smile: 😄"),
|
||||||
("x:smile:<span>s</span> :smile:", "x:smile:s 😄"),
|
("x:smile:<span>s</span> :smile:", "x:smile:s 😄"),
|
||||||
|
|||||||
Reference in new issue
Block a user