Skip to content

Commit

Permalink
feat: show link label and title (#334)
Browse files Browse the repository at this point in the history
Closes #325 

Hello!

This PR renders the various link attributes (if they exist) to prevent
lose of context/information. It's also nice to automatically style the
relevant info so the user doesn't have to manually do it when copying
markdown links from other locations.

It turns out there's two other link attributes besides the url:
- **label** this is the portion between the brackets (a Text child of
the Link node)
- **title** this is the portion between the double quotes inside the
link (this is most often shown as a tooltip when hovering over the
link/label)

```markdown
* Links look like this [](https://example.com/)
* Links with titles look like this [](https://example.com/ "Example")
* Links with labels look like this [example](https://example.com/)
* Links with titles and labels look like this [example](https://example.com/ "Example")
```


![image](https://github.com/user-attachments/assets/b1ba7e46-b24b-4304-b3de-5aec772596dc)

I'm open to formatting suggestions, let me know what you think! We might
could display this in a more markdown style (I kinda like this better):


![image](https://github.com/user-attachments/assets/e7800ca2-4f1b-4cf2-9276-f77961a47139)


Thanks!
  • Loading branch information
mfontanini authored Aug 13, 2024
2 parents a31f6e4 + 9aa1ef4 commit 66aa3a3
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 5 deletions.
73 changes: 70 additions & 3 deletions src/markdown/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,22 @@ impl<'a> InlinesParser<'a> {
NodeValue::Emph => self.process_children(node, style.italics())?,
NodeValue::Strikethrough => self.process_children(node, style.strikethrough())?,
NodeValue::SoftBreak => self.pending_text.push(Text::from(" ")),
NodeValue::Link(link) => self.pending_text.push(Text::new(link.url.clone(), TextStyle::default().link())),
NodeValue::Link(link) => {
let has_label = node.first_child().is_some();
if has_label {
self.process_children(node, TextStyle::default().link_label())?;
self.pending_text.push(Text::from(" ("));
}
self.pending_text.push(Text::new(link.url.clone(), TextStyle::default().link_url()));
if !link.title.is_empty() {
self.pending_text.push(Text::from(" \""));
self.pending_text.push(Text::new(link.title.clone(), TextStyle::default().link_title()));
self.pending_text.push(Text::from("\""));
}
if has_label {
self.pending_text.push(Text::from(")"));
}
}
NodeValue::LineBreak => {
self.store_pending_text();
self.inlines.push(Inline::LineBreak);
Expand Down Expand Up @@ -565,10 +580,62 @@ boop
}

#[test]
fn link() {
fn link_wo_label_wo_title() {
let parsed = parse_single("my [](https://example.com)");
let MarkdownElement::Paragraph(elements) = parsed else { panic!("not a paragraph: {parsed:?}") };
let expected_chunks =
vec![Text::from("my "), Text::new("https://example.com", TextStyle::default().link_url())];

let expected_elements = &[ParagraphElement::Text(TextBlock(expected_chunks))];
assert_eq!(elements, expected_elements);
}

#[test]
fn link_w_label_wo_title() {
let parsed = parse_single("my [website](https://example.com)");
let MarkdownElement::Paragraph(elements) = parsed else { panic!("not a paragraph: {parsed:?}") };
let expected_chunks = vec![Text::from("my "), Text::new("https://example.com", TextStyle::default().link())];
let expected_chunks = vec![
Text::from("my "),
Text::new("website", TextStyle::default().link_label()),
Text::from(" ("),
Text::new("https://example.com", TextStyle::default().link_url()),
Text::from(")"),
];

let expected_elements = &[ParagraphElement::Text(TextBlock(expected_chunks))];
assert_eq!(elements, expected_elements);
}

#[test]
fn link_wo_label_w_title() {
let parsed = parse_single("my [](https://example.com \"Example\")");
let MarkdownElement::Paragraph(elements) = parsed else { panic!("not a paragraph: {parsed:?}") };
let expected_chunks = vec![
Text::from("my "),
Text::new("https://example.com", TextStyle::default().link_url()),
Text::from(" \""),
Text::new("Example", TextStyle::default().link_title()),
Text::from("\""),
];

let expected_elements = &[ParagraphElement::Text(TextBlock(expected_chunks))];
assert_eq!(elements, expected_elements);
}

#[test]
fn link_w_label_w_title() {
let parsed = parse_single("my [website](https://example.com \"Example\")");
let MarkdownElement::Paragraph(elements) = parsed else { panic!("not a paragraph: {parsed:?}") };
let expected_chunks = vec![
Text::from("my "),
Text::new("website", TextStyle::default().link_label()),
Text::from(" ("),
Text::new("https://example.com", TextStyle::default().link_url()),
Text::from(" \""),
Text::new("Example", TextStyle::default().link_title()),
Text::from("\""),
Text::from(")"),
];

let expected_elements = &[ParagraphElement::Text(TextBlock(expected_chunks))];
assert_eq!(elements, expected_elements);
Expand Down
14 changes: 12 additions & 2 deletions src/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,18 @@ impl TextStyle {
self.add_flag(TextFormatFlags::Underlined)
}

/// Indicate this is a link.
pub(crate) fn link(self) -> Self {
/// Indicate this is a link label.
pub(crate) fn link_label(self) -> Self {
self.bold()
}

/// Indicate this is a link title.
pub(crate) fn link_title(self) -> Self {
self.italics()
}

/// Indicate this is a link url.
pub(crate) fn link_url(self) -> Self {
self.italics().underlined()
}

Expand Down

0 comments on commit 66aa3a3

Please sign in to comment.