Markdown: nested definitions resolve, images in styles and links stay images, CR soft breaks, reference links highlight
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
fcad0ff8e9
commit
508cc55af6
3 files changed
+254
-86
No files matched your search
@@ -6,7 +6,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||||||
|
|
||||||
use crate::{forms::Highlight, theme::ActiveTheme};
|
use crate::{forms::Highlight, theme::ActiveTheme};
|
||||||
|
|
||||||
/// The markdown this crate reads: GitHub's flavor, and math between dollars.
|
/// GitHub's flavor, and math between dollars.
|
||||||
pub(crate) fn options() -> ParseOptions {
|
pub(crate) fn options() -> ParseOptions {
|
||||||
ParseOptions {
|
ParseOptions {
|
||||||
constructs: Constructs {
|
constructs: Constructs {
|
||||||
@@ -62,7 +62,7 @@ pub(crate) fn edge(span: &str, mark: Mark) -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Every node under `node`, itself first, in source order.
|
/// Every node under `node`, itself first, in source order.
|
||||||
fn walk<'a>(node: &'a mdast::Node, out: &mut Vec<&'a mdast::Node>) {
|
pub(crate) fn walk<'a>(node: &'a mdast::Node, out: &mut Vec<&'a mdast::Node>) {
|
||||||
out.push(node);
|
out.push(node);
|
||||||
for child in node.children().into_iter().flatten() {
|
for child in node.children().into_iter().flatten() {
|
||||||
walk(child, out);
|
walk(child, out);
|
||||||
@@ -93,9 +93,10 @@ pub fn marks(text: &str) -> Vec<(Range<usize>, Mark)> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match node {
|
match node {
|
||||||
mdast::Node::Link(link) => {
|
mdast::Node::Link(mdast::Link { children, .. })
|
||||||
|
| mdast::Node::LinkReference(mdast::LinkReference { children, .. }) => {
|
||||||
out.push((range.clone(), Mark::Link));
|
out.push((range.clone(), Mark::Link));
|
||||||
if let (Some(first), Some(last)) = (link.children.first(), link.children.last()) {
|
if let (Some(first), Some(last)) = (children.first(), children.last()) {
|
||||||
let inner = span(first).start..span(last).end;
|
let inner = span(first).start..span(last).end;
|
||||||
out.push((range.start..inner.start, Mark::Syntax));
|
out.push((range.start..inner.start, Mark::Syntax));
|
||||||
out.push((inner.end..range.end, Mark::Syntax));
|
out.push((inner.end..range.end, Mark::Syntax));
|
||||||
@@ -185,7 +186,7 @@ pub fn link_at(text: &str, at: usize) -> Option<(Range<usize>, String, String)>
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The document's top-level blocks, as ranges of its source; link definitions are none.
|
/// Top-level blocks as source ranges; definitions excluded.
|
||||||
pub fn block_ranges(text: &str) -> Vec<Range<usize>> {
|
pub fn block_ranges(text: &str) -> Vec<Range<usize>> {
|
||||||
let root = tree(text);
|
let root = tree(text);
|
||||||
root.children()
|
root.children()
|
||||||
@@ -258,6 +259,24 @@ mod tests {
|
|||||||
assert_eq!(found("## Lift"), [("## Lift", Heading(2)), ("## ", Syntax)]);
|
assert_eq!(found("## Lift"), [("## Lift", Heading(2)), ("## ", Syntax)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reference_links_mark_their_syntax() {
|
||||||
|
use Mark::*;
|
||||||
|
let defined = "\n\n[site]: https://ely.dev\n";
|
||||||
|
for (link, open, close) in [
|
||||||
|
("[the site][site]", "[", "][site]"),
|
||||||
|
("[site][]", "[", "][]"),
|
||||||
|
("[site]", "[", "]"),
|
||||||
|
] {
|
||||||
|
let text = format!("{link}{defined}");
|
||||||
|
assert_eq!(
|
||||||
|
found(&text),
|
||||||
|
[(link, Link), (open, Syntax), (close, Syntax)],
|
||||||
|
"{link}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_link_gives_its_range_text_and_address() {
|
fn a_link_gives_its_range_text_and_address() {
|
||||||
let text = "see [the docs](https://ely.dev) or <https://x.dev>";
|
let text = "see [the docs](https://ely.dev) or <https://x.dev>";
|
||||||
|
|||||||
+40
-24
@@ -11,7 +11,7 @@ use super::tree::{Inline, Node, Style, parse};
|
|||||||
use crate::{
|
use crate::{
|
||||||
editor::code_colors,
|
editor::code_colors,
|
||||||
navigation::{Anchor, Sections},
|
navigation::{Anchor, Sections},
|
||||||
primitives::{Icon, IconName},
|
primitives::{FocusRing as _, Icon, IconName},
|
||||||
theme::{ActiveTheme, IconSize, Radius, TextSize},
|
theme::{ActiveTheme, IconSize, Radius, TextSize},
|
||||||
typography::Latex,
|
typography::Latex,
|
||||||
};
|
};
|
||||||
@@ -146,12 +146,7 @@ impl Draw<'_> {
|
|||||||
let on_link = self.on_link.clone();
|
let on_link = self.on_link.clone();
|
||||||
InteractiveText::new(self.key("links"), styled)
|
InteractiveText::new(self.key("links"), styled)
|
||||||
.on_click(ranges, move |ix, window, cx| {
|
.on_click(ranges, move |ix, window, cx| {
|
||||||
let url = &urls[ix];
|
open_link(&on_link, &urls[ix], window, cx)
|
||||||
log::info!("markdown: open {url}");
|
|
||||||
match &on_link {
|
|
||||||
Some(on_link) => on_link(url, window, cx),
|
|
||||||
None => cx.open_url(url),
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -348,23 +343,35 @@ impl Draw<'_> {
|
|||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
})
|
})
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
Node::Image { alt, url } => div()
|
Node::Image { alt, url, link } => {
|
||||||
.flex()
|
let picture = div()
|
||||||
.items_center()
|
.id(self.key("image"))
|
||||||
.gap_2()
|
.flex()
|
||||||
.p_3()
|
.items_center()
|
||||||
.rounded(theme.radius(Radius::Md))
|
.gap_2()
|
||||||
.border_1()
|
.p_3()
|
||||||
.border_color(colors.border)
|
.rounded(theme.radius(Radius::Md))
|
||||||
.text_color(colors.fg_muted)
|
.border_1()
|
||||||
.child(
|
.border_color(colors.border)
|
||||||
Icon::new(IconName::Image)
|
.text_color(colors.fg_muted)
|
||||||
.size(IconSize::Sm)
|
.child(
|
||||||
.color(colors.fg_subtle),
|
Icon::new(IconName::Image)
|
||||||
)
|
.size(IconSize::Sm)
|
||||||
.child(div().flex_1().child(alt.clone()))
|
.color(colors.fg_subtle),
|
||||||
.child(div().text_color(colors.fg_subtle).child(url.clone()))
|
)
|
||||||
.into_any_element(),
|
.child(div().flex_1().child(alt.clone()))
|
||||||
|
.child(div().text_color(colors.fg_subtle).child(url.clone()));
|
||||||
|
let Some(link) = link.clone() else {
|
||||||
|
return picture.into_any_element();
|
||||||
|
};
|
||||||
|
let on_link = self.on_link.clone();
|
||||||
|
picture
|
||||||
|
.tab_index(0)
|
||||||
|
.focus_ring(cx)
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_click(move |_, window, cx| open_link(&on_link, &link, window, cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
Node::Footnote { label, body } => div()
|
Node::Footnote { label, body } => div()
|
||||||
.flex()
|
.flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
@@ -383,6 +390,15 @@ impl Draw<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Opens `url` through the owner, else the system.
|
||||||
|
fn open_link(on_link: &Option<OnLink>, url: &str, window: &mut Window, cx: &mut App) {
|
||||||
|
log::info!("markdown: open {url}");
|
||||||
|
match on_link {
|
||||||
|
Some(on_link) => on_link(url, window, cx),
|
||||||
|
None => cx.open_url(url),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The anchor of the document's `ordinal`th heading.
|
/// The anchor of the document's `ordinal`th heading.
|
||||||
fn anchor(ordinal: usize) -> SharedString {
|
fn anchor(ordinal: usize) -> SharedString {
|
||||||
format!("heading-{ordinal}").into()
|
format!("heading-{ordinal}").into()
|
||||||
|
|||||||
+190
-57
@@ -2,7 +2,7 @@ use std::{collections::HashMap, ops::Range};
|
|||||||
|
|
||||||
use markdown::mdast::{self, AlignKind};
|
use markdown::mdast::{self, AlignKind};
|
||||||
|
|
||||||
use super::markdown::{span, tree};
|
use super::markdown::{span, tree, walk};
|
||||||
|
|
||||||
/// How a stretch of inline text prints.
|
/// How a stretch of inline text prints.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
@@ -46,9 +46,11 @@ pub enum Node {
|
|||||||
},
|
},
|
||||||
Rule,
|
Rule,
|
||||||
Math(String),
|
Math(String),
|
||||||
|
/// A picture, and the link around it.
|
||||||
Image {
|
Image {
|
||||||
alt: String,
|
alt: String,
|
||||||
url: String,
|
url: String,
|
||||||
|
link: Option<String>,
|
||||||
},
|
},
|
||||||
Footnote {
|
Footnote {
|
||||||
label: String,
|
label: String,
|
||||||
@@ -62,13 +64,44 @@ struct Reader<'a> {
|
|||||||
urls: HashMap<String, String>,
|
urls: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inline text being built, and the containers open around it.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Run {
|
||||||
|
text: Inline,
|
||||||
|
open: Vec<(usize, Style, Option<String>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Run {
|
||||||
|
/// Text so far; open styles close here, reopen at zero.
|
||||||
|
fn cut(&mut self) -> Inline {
|
||||||
|
let mut done = std::mem::take(&mut self.text);
|
||||||
|
let end = done.text.len();
|
||||||
|
for (start, style, url) in self.open.iter_mut().rev() {
|
||||||
|
if *start < end {
|
||||||
|
done.styles.push((*start..end, *style));
|
||||||
|
if let Some(url) = url {
|
||||||
|
done.links.push((*start..end, url.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*start = 0;
|
||||||
|
}
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The innermost open link's address.
|
||||||
|
fn link(&self) -> Option<String> {
|
||||||
|
self.open.iter().rev().find_map(|(_, _, url)| url.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `markdown` as blocks.
|
/// `markdown` as blocks.
|
||||||
pub fn parse(markdown: &str) -> Vec<Node> {
|
pub fn parse(markdown: &str) -> Vec<Node> {
|
||||||
let root = tree(markdown);
|
let root = tree(markdown);
|
||||||
let blocks = root.children().expect("the root holds the blocks");
|
let mut nodes = Vec::new();
|
||||||
|
walk(&root, &mut nodes);
|
||||||
let mut urls = HashMap::new();
|
let mut urls = HashMap::new();
|
||||||
for block in blocks {
|
for node in nodes {
|
||||||
if let mdast::Node::Definition(definition) = block {
|
if let mdast::Node::Definition(definition) = node {
|
||||||
urls.entry(definition.identifier.clone())
|
urls.entry(definition.identifier.clone())
|
||||||
.or_insert_with(|| definition.url.clone());
|
.or_insert_with(|| definition.url.clone());
|
||||||
}
|
}
|
||||||
@@ -78,7 +111,10 @@ pub fn parse(markdown: &str) -> Vec<Node> {
|
|||||||
urls,
|
urls,
|
||||||
};
|
};
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
reader.blocks(blocks, &mut out);
|
reader.blocks(
|
||||||
|
root.children().expect("the root holds the blocks"),
|
||||||
|
&mut out,
|
||||||
|
);
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +131,7 @@ impl Reader<'_> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The address a reference names: the parser makes references to definitions alone.
|
/// A reference's address; the parser checked its definition.
|
||||||
fn url(&self, identifier: &str) -> String {
|
fn url(&self, identifier: &str) -> String {
|
||||||
self.urls
|
self.urls
|
||||||
.get(identifier)
|
.get(identifier)
|
||||||
@@ -107,7 +143,12 @@ impl Reader<'_> {
|
|||||||
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) => {
|
||||||
out.push(Node::Heading(heading.depth, self.inline(&heading.children)))
|
let (mut run, mut after) = (Run::default(), Vec::new());
|
||||||
|
for child in &heading.children {
|
||||||
|
self.inline_into(child, &mut run, &mut Images::After(&mut after));
|
||||||
|
}
|
||||||
|
out.push(Node::Heading(heading.depth, run.cut()));
|
||||||
|
out.extend(after);
|
||||||
}
|
}
|
||||||
mdast::Node::Blockquote(quote) => out.push(Node::Quote(self.nested("e.children))),
|
mdast::Node::Blockquote(quote) => out.push(Node::Quote(self.nested("e.children))),
|
||||||
mdast::Node::Code(code) => out.push(Node::Code {
|
mdast::Node::Code(code) => out.push(Node::Code {
|
||||||
@@ -156,53 +197,36 @@ impl Reader<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A paragraph's text split around its images; `$$` math written alone stands as a block.
|
/// Text split at its images; lone `$$` math is block.
|
||||||
fn paragraph(&self, node: &mdast::Node, children: &[mdast::Node], out: &mut Vec<Node>) {
|
fn paragraph(&self, node: &mdast::Node, children: &[mdast::Node], out: &mut Vec<Node>) {
|
||||||
if let [mdast::Node::InlineMath(math)] = children
|
if let [mdast::Node::InlineMath(math)] = children
|
||||||
&& self.source[span(node)].starts_with("$$")
|
&& self.source[span(node)].starts_with("$$")
|
||||||
{
|
{
|
||||||
return out.push(Node::Math(math.value.clone()));
|
return out.push(Node::Math(math.value.clone()));
|
||||||
}
|
}
|
||||||
let mut run = Inline::default();
|
let mut run = Run::default();
|
||||||
for child in children {
|
for child in children {
|
||||||
let image = match child {
|
self.inline_into(child, &mut run, &mut Images::Split(out));
|
||||||
mdast::Node::Image(image) => Some((image.alt.clone(), image.url.clone())),
|
|
||||||
mdast::Node::ImageReference(image) => {
|
|
||||||
Some((image.alt.clone(), self.url(&image.identifier)))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
match image {
|
|
||||||
Some((alt, url)) => {
|
|
||||||
text_block(std::mem::take(&mut run), out);
|
|
||||||
out.push(Node::Image { alt, url });
|
|
||||||
}
|
|
||||||
None => self.inline_into(child, &mut run),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
text_block(run, out);
|
text_block(run.cut(), out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inline text where images draw as their alt text.
|
||||||
fn inline(&self, nodes: &[mdast::Node]) -> Inline {
|
fn inline(&self, nodes: &[mdast::Node]) -> Inline {
|
||||||
let mut text = Inline::default();
|
let mut run = Run::default();
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
self.inline_into(node, &mut text);
|
self.inline_into(node, &mut run, &mut Images::Alt);
|
||||||
}
|
}
|
||||||
text
|
run.cut()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `nodes` into `text`; their stretch of it, styled.
|
fn inline_into(&self, node: &mdast::Node, run: &mut Run, images: &mut Images) {
|
||||||
fn styled_children(&self, nodes: &[mdast::Node], text: &mut Inline) -> Range<usize> {
|
let text = &mut run.text;
|
||||||
let start = text.text.len();
|
let (children, style, url) = match node {
|
||||||
for node in nodes {
|
mdast::Node::Text(words) => {
|
||||||
self.inline_into(node, text);
|
let soft = words.value.replace("\r\n", " ").replace(['\r', '\n'], " ");
|
||||||
}
|
return text.text.push_str(&soft);
|
||||||
start..text.text.len()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn inline_into(&self, node: &mdast::Node, text: &mut Inline) {
|
|
||||||
let (children, style) = match node {
|
|
||||||
mdast::Node::Text(words) => return text.text.push_str(&words.value.replace('\n', " ")),
|
|
||||||
mdast::Node::Break(_) => return text.text.push('\n'),
|
mdast::Node::Break(_) => return text.text.push('\n'),
|
||||||
mdast::Node::InlineCode(code) => return styled(text, &code.value, Style::Code),
|
mdast::Node::InlineCode(code) => return styled(text, &code.value, Style::Code),
|
||||||
mdast::Node::InlineMath(math) => return styled(text, &math.value, Style::Math),
|
mdast::Node::InlineMath(math) => return styled(text, &math.value, Style::Math),
|
||||||
@@ -210,28 +234,61 @@ impl Reader<'_> {
|
|||||||
let label = note.label.as_ref().unwrap_or(¬e.identifier);
|
let label = note.label.as_ref().unwrap_or(¬e.identifier);
|
||||||
return styled(text, &format!("[{label}]"), Style::Footnote);
|
return styled(text, &format!("[{label}]"), Style::Footnote);
|
||||||
}
|
}
|
||||||
mdast::Node::Image(image) => return text.text.push_str(&image.alt),
|
mdast::Node::Image(_) | mdast::Node::ImageReference(_) => {
|
||||||
mdast::Node::ImageReference(image) => return text.text.push_str(&image.alt),
|
let (alt, url) = match node {
|
||||||
|
mdast::Node::Image(image) => (image.alt.clone(), image.url.clone()),
|
||||||
|
mdast::Node::ImageReference(image) => {
|
||||||
|
(image.alt.clone(), self.url(&image.identifier))
|
||||||
|
}
|
||||||
|
_ => unreachable!("matched as an image"),
|
||||||
|
};
|
||||||
|
let link = run.link();
|
||||||
|
return match images {
|
||||||
|
Images::Alt => run.text.text.push_str(&alt),
|
||||||
|
Images::Split(out) => {
|
||||||
|
text_block(run.cut(), out);
|
||||||
|
out.push(Node::Image { alt, url, link })
|
||||||
|
}
|
||||||
|
Images::After(out) => out.push(Node::Image { alt, url, link }),
|
||||||
|
};
|
||||||
|
}
|
||||||
mdast::Node::Html(_) => return,
|
mdast::Node::Html(_) => return,
|
||||||
mdast::Node::Strong(strong) => (&strong.children, Style::Strong),
|
mdast::Node::Strong(strong) => (&strong.children, Style::Strong, None),
|
||||||
mdast::Node::Emphasis(emphasis) => (&emphasis.children, Style::Emphasis),
|
mdast::Node::Emphasis(emphasis) => (&emphasis.children, Style::Emphasis, None),
|
||||||
mdast::Node::Delete(delete) => (&delete.children, Style::Strike),
|
mdast::Node::Delete(delete) => (&delete.children, Style::Strike, None),
|
||||||
mdast::Node::Link(link) => (&link.children, Style::Link),
|
mdast::Node::Link(link) => (&link.children, Style::Link, Some(link.url.clone())),
|
||||||
mdast::Node::LinkReference(link) => (&link.children, Style::Link),
|
mdast::Node::LinkReference(link) => (
|
||||||
|
&link.children,
|
||||||
|
Style::Link,
|
||||||
|
Some(self.url(&link.identifier)),
|
||||||
|
),
|
||||||
other => unreachable!("{other:?} is no inline outside MDX"),
|
other => unreachable!("{other:?} is no inline outside MDX"),
|
||||||
};
|
};
|
||||||
let range = self.styled_children(children, text);
|
run.open.push((run.text.text.len(), style, url));
|
||||||
text.styles.push((range.clone(), style));
|
for child in children {
|
||||||
match node {
|
self.inline_into(child, run, images);
|
||||||
mdast::Node::Link(link) => text.links.push((range, link.url.clone())),
|
}
|
||||||
mdast::Node::LinkReference(link) => {
|
let (start, style, url) = run.open.pop().expect("the container opened above");
|
||||||
text.links.push((range, self.url(&link.identifier)))
|
let end = run.text.text.len();
|
||||||
|
if start < end {
|
||||||
|
run.text.styles.push((start..end, style));
|
||||||
|
if let Some(url) = url {
|
||||||
|
run.text.links.push((start..end, url));
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where an inline image goes.
|
||||||
|
enum Images<'a> {
|
||||||
|
/// Its alt text stays in the line.
|
||||||
|
Alt,
|
||||||
|
/// It splits the text into blocks.
|
||||||
|
Split(&'a mut Vec<Node>),
|
||||||
|
/// It follows the text, as a heading's.
|
||||||
|
After(&'a mut Vec<Node>),
|
||||||
|
}
|
||||||
|
|
||||||
/// Text left between images, unless it is blank.
|
/// Text left between images, unless it is blank.
|
||||||
fn text_block(text: Inline, out: &mut Vec<Node>) {
|
fn text_block(text: Inline, out: &mut Vec<Node>) {
|
||||||
if !text.text.trim().is_empty() {
|
if !text.text.trim().is_empty() {
|
||||||
@@ -287,7 +344,8 @@ mod tests {
|
|||||||
parse("](outer.png)"),
|
parse("](outer.png)"),
|
||||||
[Node::Image {
|
[Node::Image {
|
||||||
alt: "outer ".into(),
|
alt: "outer ".into(),
|
||||||
url: "outer.png".into()
|
url: "outer.png".into(),
|
||||||
|
link: None
|
||||||
}]
|
}]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -301,7 +359,8 @@ mod tests {
|
|||||||
nodes[1],
|
nodes[1],
|
||||||
Node::Image {
|
Node::Image {
|
||||||
alt: "a".into(),
|
alt: "a".into(),
|
||||||
url: "a.png".into()
|
url: "a.png".into(),
|
||||||
|
link: None
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
let Node::Paragraph(after) = &nodes[2] else {
|
let Node::Paragraph(after) = &nodes[2] else {
|
||||||
@@ -314,8 +373,82 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_soft_break_reads_as_a_space_and_a_hard_one_as_a_line() {
|
fn a_soft_break_reads_as_a_space_and_a_hard_one_as_a_line() {
|
||||||
let nodes = parse("one\ntwo \nthree");
|
for (source, shown) in [
|
||||||
assert!(matches!(&nodes[0], Node::Paragraph(text) if text.text == "one two\nthree"));
|
("one\ntwo \nthree", "one two\nthree"),
|
||||||
|
("one\r\ntwo \r\nthree", "one two\nthree"),
|
||||||
|
("one\rtwo \rthree", "one two\nthree"),
|
||||||
|
] {
|
||||||
|
let nodes = parse(source);
|
||||||
|
assert!(
|
||||||
|
matches!(&nodes[0], Node::Paragraph(text) if text.text == shown),
|
||||||
|
"{source:?} reads {nodes:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn definitions_nested_in_quotes_and_lists_resolve() {
|
||||||
|
for source in [
|
||||||
|
"[site] ![pic]\n\n> [site]: https://ely.dev\n> [pic]: a.png\n",
|
||||||
|
"[site] ![pic]\n\n- [site]: https://ely.dev\n- [pic]: a.png\n",
|
||||||
|
] {
|
||||||
|
let nodes = parse(source);
|
||||||
|
let Node::Paragraph(text) = &nodes[0] else {
|
||||||
|
panic!("a paragraph, not {:?}", nodes[0])
|
||||||
|
};
|
||||||
|
assert_eq!(text.links, [(0..4, "https://ely.dev".to_string())]);
|
||||||
|
assert_eq!(
|
||||||
|
nodes[1],
|
||||||
|
Node::Image {
|
||||||
|
alt: "pic".into(),
|
||||||
|
url: "a.png".into(),
|
||||||
|
link: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_image_inside_styles_and_links_stays_an_image() {
|
||||||
|
let nodes = parse("*a  b* [](https://ely.dev)");
|
||||||
|
let Node::Paragraph(before) = &nodes[0] else {
|
||||||
|
panic!("a paragraph, not {:?}", nodes[0])
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
(before.text.as_str(), &before.styles[..]),
|
||||||
|
("a ", &[(0..2, Style::Emphasis)][..])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
nodes[1],
|
||||||
|
Node::Image {
|
||||||
|
alt: "x".into(),
|
||||||
|
url: "x.png".into(),
|
||||||
|
link: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let Node::Paragraph(after) = &nodes[2] else {
|
||||||
|
panic!("a paragraph, not {:?}", nodes[2])
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
(after.text.as_str(), &after.styles[..]),
|
||||||
|
(" b ", &[(0..2, Style::Emphasis)][..])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
nodes[3],
|
||||||
|
Node::Image {
|
||||||
|
alt: "y".into(),
|
||||||
|
url: "y.png".into(),
|
||||||
|
link: Some("https://ely.dev".into())
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(nodes.len(), 4, "{nodes:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_heading_keeps_its_images_after_it() {
|
||||||
|
let nodes = parse("# Lift ");
|
||||||
|
assert!(matches!(&nodes[0], Node::Heading(1, text) if text.text == "Lift "));
|
||||||
|
assert!(matches!(&nodes[1], Node::Image { url, .. } if url == "x.png"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user