Support rendering strikethrough text in markdown (#8287)
Just noticed strikethrough text handling was not implemented for the following: Chat  Markdown Preview  Code Documentation  It looks like there are three different markdown parsing/rendering implementations, might be worth to investigate if any of these can be combined into a single crate (looks like a lot of work though). Release Notes: - Added support for rendering strikethrough text in markdown elements
This commit is contained in:
parent
cd8ede542b
commit
43163a0154
6 changed files with 187 additions and 69 deletions
|
@ -1,4 +1,6 @@
|
|||
use gpui::{px, FontStyle, FontWeight, HighlightStyle, SharedString, UnderlineStyle};
|
||||
use gpui::{
|
||||
px, FontStyle, FontWeight, HighlightStyle, SharedString, StrikethroughStyle, UnderlineStyle,
|
||||
};
|
||||
use language::HighlightId;
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
|
||||
|
@ -170,6 +172,13 @@ impl MarkdownHighlight {
|
|||
});
|
||||
}
|
||||
|
||||
if style.strikethrough {
|
||||
highlight.strikethrough = Some(StrikethroughStyle {
|
||||
thickness: px(1.),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if style.weight != FontWeight::default() {
|
||||
highlight.font_weight = Some(style.weight);
|
||||
}
|
||||
|
@ -189,6 +198,8 @@ pub struct MarkdownHighlightStyle {
|
|||
pub italic: bool,
|
||||
/// Whether the text should be underlined.
|
||||
pub underline: bool,
|
||||
/// Whether the text should be struck through.
|
||||
pub strikethrough: bool,
|
||||
/// The weight of the text.
|
||||
pub weight: FontWeight,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::markdown_elements::*;
|
||||
use gpui::FontWeight;
|
||||
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag};
|
||||
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
|
||||
use std::{ops::Range, path::PathBuf};
|
||||
|
||||
pub fn parse_markdown(
|
||||
|
@ -70,11 +70,11 @@ impl<'a> MarkdownParser<'a> {
|
|||
| Event::Code(_)
|
||||
| Event::Html(_)
|
||||
| Event::FootnoteReference(_)
|
||||
| Event::Start(Tag::Link(_, _, _))
|
||||
| Event::Start(Tag::Link { link_type: _, dest_url: _, title: _, id: _ })
|
||||
| Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::Start(Tag::Strikethrough)
|
||||
| Event::Start(Tag::Image(_, _, _)) => {
|
||||
| Event::Start(Tag::Image { link_type: _, dest_url: _, title: _, id: _ }) => {
|
||||
return true;
|
||||
}
|
||||
_ => return false,
|
||||
|
@ -99,15 +99,21 @@ impl<'a> MarkdownParser<'a> {
|
|||
let text = self.parse_text(false);
|
||||
Some(ParsedMarkdownElement::Paragraph(text))
|
||||
}
|
||||
Tag::Heading(level, _, _) => {
|
||||
Tag::Heading {
|
||||
level,
|
||||
id: _,
|
||||
classes: _,
|
||||
attrs: _,
|
||||
} => {
|
||||
let level = level.clone();
|
||||
self.cursor += 1;
|
||||
let heading = self.parse_heading(level);
|
||||
Some(ParsedMarkdownElement::Heading(heading))
|
||||
}
|
||||
Tag::Table(_) => {
|
||||
Tag::Table(alignment) => {
|
||||
let alignment = alignment.clone();
|
||||
self.cursor += 1;
|
||||
let table = self.parse_table();
|
||||
let table = self.parse_table(alignment);
|
||||
Some(ParsedMarkdownElement::Table(table))
|
||||
}
|
||||
Tag::List(order) => {
|
||||
|
@ -162,6 +168,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
let mut text = String::new();
|
||||
let mut bold_depth = 0;
|
||||
let mut italic_depth = 0;
|
||||
let mut strikethrough_depth = 0;
|
||||
let mut link: Option<Link> = None;
|
||||
let mut region_ranges: Vec<Range<usize>> = vec![];
|
||||
let mut regions: Vec<ParsedRegion> = vec![];
|
||||
|
@ -201,6 +208,10 @@ impl<'a> MarkdownParser<'a> {
|
|||
style.italic = true;
|
||||
}
|
||||
|
||||
if strikethrough_depth > 0 {
|
||||
style.strikethrough = true;
|
||||
}
|
||||
|
||||
if let Some(link) = link.clone() {
|
||||
region_ranges.push(prev_len..text.len());
|
||||
regions.push(ParsedRegion {
|
||||
|
@ -248,39 +259,40 @@ impl<'a> MarkdownParser<'a> {
|
|||
});
|
||||
}
|
||||
|
||||
Event::Start(tag) => {
|
||||
match tag {
|
||||
Tag::Emphasis => italic_depth += 1,
|
||||
Tag::Strong => bold_depth += 1,
|
||||
Tag::Link(_type, url, _title) => {
|
||||
link = Link::identify(
|
||||
self.file_location_directory.clone(),
|
||||
url.to_string(),
|
||||
);
|
||||
}
|
||||
Tag::Strikethrough => {
|
||||
// TODO: Confirm that gpui currently doesn't support strikethroughs
|
||||
}
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
Event::Start(tag) => match tag {
|
||||
Tag::Emphasis => italic_depth += 1,
|
||||
Tag::Strong => bold_depth += 1,
|
||||
Tag::Strikethrough => strikethrough_depth += 1,
|
||||
Tag::Link {
|
||||
link_type: _,
|
||||
dest_url,
|
||||
title: _,
|
||||
id: _,
|
||||
} => {
|
||||
link = Link::identify(
|
||||
self.file_location_directory.clone(),
|
||||
dest_url.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
Event::End(tag) => match tag {
|
||||
Tag::Emphasis => {
|
||||
TagEnd::Emphasis => {
|
||||
italic_depth -= 1;
|
||||
}
|
||||
Tag::Strong => {
|
||||
TagEnd::Strong => {
|
||||
bold_depth -= 1;
|
||||
}
|
||||
Tag::Link(_, _, _) => {
|
||||
TagEnd::Strikethrough => {
|
||||
strikethrough_depth -= 1;
|
||||
}
|
||||
TagEnd::Link => {
|
||||
link = None;
|
||||
}
|
||||
Tag::Strikethrough => {
|
||||
// TODO: Confirm that gpui currently doesn't support strikethroughs
|
||||
}
|
||||
Tag::Paragraph => {
|
||||
TagEnd::Paragraph => {
|
||||
self.cursor += 1;
|
||||
break;
|
||||
}
|
||||
|
@ -328,14 +340,17 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_table(&mut self) -> ParsedMarkdownTable {
|
||||
fn parse_table(&mut self, alignment: Vec<Alignment>) -> ParsedMarkdownTable {
|
||||
let (_event, source_range) = self.previous().unwrap();
|
||||
let source_range = source_range.clone();
|
||||
let mut header = ParsedMarkdownTableRow::new();
|
||||
let mut body = vec![];
|
||||
let mut current_row = vec![];
|
||||
let mut in_header = true;
|
||||
let mut alignment: Vec<ParsedMarkdownTableAlignment> = vec![];
|
||||
let column_alignments = alignment
|
||||
.iter()
|
||||
.map(|a| Self::convert_alignment(a))
|
||||
.collect();
|
||||
|
||||
loop {
|
||||
if self.eof() {
|
||||
|
@ -346,7 +361,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
match current {
|
||||
Event::Start(Tag::TableHead)
|
||||
| Event::Start(Tag::TableRow)
|
||||
| Event::End(Tag::TableCell) => {
|
||||
| Event::End(TagEnd::TableCell) => {
|
||||
self.cursor += 1;
|
||||
}
|
||||
Event::Start(Tag::TableCell) => {
|
||||
|
@ -354,7 +369,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
let cell_contents = self.parse_text(false);
|
||||
current_row.push(cell_contents);
|
||||
}
|
||||
Event::End(Tag::TableHead) | Event::End(Tag::TableRow) => {
|
||||
Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => {
|
||||
self.cursor += 1;
|
||||
let new_row = std::mem::replace(&mut current_row, vec![]);
|
||||
if in_header {
|
||||
|
@ -365,11 +380,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
body.push(row);
|
||||
}
|
||||
}
|
||||
Event::End(Tag::Table(table_alignment)) => {
|
||||
alignment = table_alignment
|
||||
.iter()
|
||||
.map(|a| Self::convert_alignment(a))
|
||||
.collect();
|
||||
Event::End(TagEnd::Table) => {
|
||||
self.cursor += 1;
|
||||
break;
|
||||
}
|
||||
|
@ -383,7 +394,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
source_range,
|
||||
header,
|
||||
body,
|
||||
column_alignments: alignment,
|
||||
column_alignments,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -417,7 +428,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
let block = ParsedMarkdownElement::List(inner_list);
|
||||
current_list_items.push(Box::new(block));
|
||||
}
|
||||
Event::End(Tag::List(_)) => {
|
||||
Event::End(TagEnd::List(_)) => {
|
||||
self.cursor += 1;
|
||||
break;
|
||||
}
|
||||
|
@ -451,7 +462,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
}
|
||||
}
|
||||
}
|
||||
Event::End(Tag::Item) => {
|
||||
Event::End(TagEnd::Item) => {
|
||||
self.cursor += 1;
|
||||
|
||||
let item_type = if let Some(checked) = task_item {
|
||||
|
@ -525,7 +536,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
Event::Start(Tag::BlockQuote) => {
|
||||
nested_depth += 1;
|
||||
}
|
||||
Event::End(Tag::BlockQuote) => {
|
||||
Event::End(TagEnd::BlockQuote) => {
|
||||
nested_depth -= 1;
|
||||
if nested_depth == 0 {
|
||||
self.cursor += 1;
|
||||
|
@ -554,7 +565,7 @@ impl<'a> MarkdownParser<'a> {
|
|||
code.push_str(&text);
|
||||
self.cursor += 1;
|
||||
}
|
||||
Event::End(Tag::CodeBlock(_)) => {
|
||||
Event::End(TagEnd::CodeBlock) => {
|
||||
self.cursor += 1;
|
||||
break;
|
||||
}
|
||||
|
@ -642,6 +653,56 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_bold_strikethrough_text() {
|
||||
let parsed = parse("Some **bo~~strikethrough~~ld** text");
|
||||
|
||||
assert_eq!(parsed.children.len(), 1);
|
||||
assert_eq!(
|
||||
parsed.children[0],
|
||||
ParsedMarkdownElement::Paragraph(ParsedMarkdownText {
|
||||
source_range: 0..35,
|
||||
contents: "Some bostrikethroughld text".to_string(),
|
||||
highlights: Vec::new(),
|
||||
region_ranges: Vec::new(),
|
||||
regions: Vec::new(),
|
||||
})
|
||||
);
|
||||
|
||||
let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
|
||||
text
|
||||
} else {
|
||||
panic!("Expected a paragraph");
|
||||
};
|
||||
assert_eq!(
|
||||
paragraph.highlights,
|
||||
vec![
|
||||
(
|
||||
5..7,
|
||||
MarkdownHighlight::Style(MarkdownHighlightStyle {
|
||||
weight: FontWeight::BOLD,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
(
|
||||
7..20,
|
||||
MarkdownHighlight::Style(MarkdownHighlightStyle {
|
||||
weight: FontWeight::BOLD,
|
||||
strikethrough: true,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
(
|
||||
20..22,
|
||||
MarkdownHighlight::Style(MarkdownHighlightStyle {
|
||||
weight: FontWeight::BOLD,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_header_only_table() {
|
||||
let markdown = "\
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue