text_view: Fix inline image render. (#1166)
GPUI does not support inline image into an `InteractiveText` yet. So here, let it render into a block line. <img width="1201" height="902" alt="image" src="https://github.com/user-attachments/assets/797baeae-8764-417a-a907-e0d22956bf62" /> <img width="1064" height="805" alt="image" src="https://github.com/user-attachments/assets/a5ef7000-aa71-4c1b-8a82-2a0f36649fd6" />
This commit is contained in:
parent
8ed135d41a
commit
554082b969
4 changed files with 275 additions and 219 deletions
|
|
@ -1,5 +1,7 @@
|
|||
# Hello, **World**!
|
||||
|
||||
Build Status [](https://github.com/longbridge/gpui-component/actions/workflows/ci.yml) of [GPUI Component](https://github.com/longbridge/gpui-component).
|
||||
|
||||
This is first paragraph, there have **BOLD**, _italic_, and ~strikethrough~, `code` text.
|
||||
|
||||
This is an additional demonstration paragraph in English demonstrating more content for [Markdown GFM](https://github.github.com/gfm/). It includes various stylistic elements and plain text.
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@ use gpui::{
|
|||
div, img, prelude::FluentBuilder as _, px, relative, rems, AnyElement, App, DefiniteLength,
|
||||
Div, ElementId, FontStyle, FontWeight, Half, HighlightStyle, InteractiveElement as _,
|
||||
InteractiveText, IntoElement, Length, ObjectFit, ParentElement, Rems, RenderOnce, SharedString,
|
||||
SharedUri, Styled, StyledImage as _, StyledText, Window,
|
||||
SharedUri, StatefulInteractiveElement, Styled, StyledImage as _, StyledText, Window,
|
||||
};
|
||||
use markdown::mdast;
|
||||
|
||||
use crate::{h_flex, highlighter::SyntaxHighlighter, v_flex, ActiveTheme as _, Icon, IconName};
|
||||
use crate::{
|
||||
h_flex, highlighter::SyntaxHighlighter, tooltip::Tooltip, v_flex, ActiveTheme as _, Icon,
|
||||
IconName,
|
||||
};
|
||||
|
||||
use super::{utils::list_item_prefix, TextViewStyle};
|
||||
|
||||
|
|
@ -44,12 +47,22 @@ impl From<Span> for ElementId {
|
|||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ImageNode {
|
||||
pub url: SharedUri,
|
||||
pub link: Option<LinkMark>,
|
||||
pub title: Option<SharedString>,
|
||||
pub alt: Option<SharedString>,
|
||||
pub width: Option<DefiniteLength>,
|
||||
pub height: Option<DefiniteLength>,
|
||||
}
|
||||
|
||||
impl ImageNode {
|
||||
pub fn title(&self) -> String {
|
||||
self.title
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.alt.clone().unwrap_or_default())
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ImageNode {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.url == other.url && self.title == other.title && self.alt == other.alt
|
||||
|
|
@ -60,37 +73,24 @@ impl PartialEq for ImageNode {
|
|||
pub struct TextNode {
|
||||
/// The text content.
|
||||
pub text: String,
|
||||
pub image: Option<ImageNode>,
|
||||
/// The text styles, each tuple contains the range of the text and the style.
|
||||
pub marks: Vec<(Range<usize>, InlineTextStyle)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, IntoElement)]
|
||||
pub enum Paragraph {
|
||||
Texts {
|
||||
span: Option<Span>,
|
||||
children: Vec<TextNode>,
|
||||
},
|
||||
Image {
|
||||
span: Option<Span>,
|
||||
image: ImageNode,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for Paragraph {
|
||||
fn default() -> Self {
|
||||
Self::Texts {
|
||||
span: None,
|
||||
children: vec![],
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default, Clone, PartialEq, IntoElement)]
|
||||
pub struct Paragraph {
|
||||
pub(super) span: Option<Span>,
|
||||
pub(super) children: Vec<TextNode>,
|
||||
}
|
||||
|
||||
impl From<String> for Paragraph {
|
||||
fn from(value: String) -> Self {
|
||||
Self::Texts {
|
||||
Self {
|
||||
span: None,
|
||||
children: vec![TextNode {
|
||||
text: value.clone(),
|
||||
image: None,
|
||||
marks: vec![],
|
||||
}],
|
||||
}
|
||||
|
|
@ -141,80 +141,60 @@ pub struct TableCell {
|
|||
|
||||
impl Paragraph {
|
||||
pub fn clear(&mut self) {
|
||||
match self {
|
||||
Self::Texts { children, .. } => children.clear(),
|
||||
Self::Image { .. } => *self = Self::default(),
|
||||
}
|
||||
self.span = None;
|
||||
self.children.clear();
|
||||
}
|
||||
|
||||
pub fn is_image(&self) -> bool {
|
||||
matches!(self, Self::Image { .. })
|
||||
false
|
||||
}
|
||||
|
||||
pub fn set_span(&mut self, span: Span) {
|
||||
match self {
|
||||
Self::Texts { span: s, .. } => *s = Some(span),
|
||||
Self::Image { span: s, .. } => *s = Some(span),
|
||||
}
|
||||
self.span = Some(span);
|
||||
}
|
||||
|
||||
pub fn push_str(&mut self, text: &str) {
|
||||
if let Self::Texts { children, .. } = self {
|
||||
children.push(TextNode {
|
||||
text: text.to_string(),
|
||||
marks: vec![(0..text.len(), InlineTextStyle::default())],
|
||||
});
|
||||
}
|
||||
self.children.push(TextNode {
|
||||
text: text.to_string(),
|
||||
image: None,
|
||||
marks: vec![(0..text.len(), InlineTextStyle::default())],
|
||||
});
|
||||
}
|
||||
|
||||
pub fn push(&mut self, text: TextNode) {
|
||||
if let Self::Texts { children, .. } = self {
|
||||
children.push(text);
|
||||
}
|
||||
self.children.push(text);
|
||||
}
|
||||
|
||||
pub fn set_image(&mut self, image: ImageNode) {
|
||||
*self = Self::Image { span: None, image };
|
||||
pub fn push_image(&mut self, image: ImageNode) {
|
||||
self.children.push(TextNode {
|
||||
text: String::new(),
|
||||
image: Some(image),
|
||||
marks: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::Texts { .. } => self.text_len() == 0,
|
||||
Self::Image { .. } => false,
|
||||
}
|
||||
self.children.is_empty()
|
||||
|| self
|
||||
.children
|
||||
.iter()
|
||||
.all(|node| node.text.is_empty() && node.image.is_none())
|
||||
}
|
||||
|
||||
/// Return length of children text.
|
||||
pub fn text_len(&self) -> usize {
|
||||
match self {
|
||||
Self::Texts { children, .. } => {
|
||||
let mut len = 0;
|
||||
for text_node in children.iter() {
|
||||
len = text_node.text.len().max(len);
|
||||
}
|
||||
len
|
||||
}
|
||||
Self::Image { .. } => 1,
|
||||
}
|
||||
self.children
|
||||
.iter()
|
||||
.map(|node| node.text.len())
|
||||
.sum::<usize>()
|
||||
}
|
||||
|
||||
/// Try to merge two paragraphs, if they are both text elements.
|
||||
///
|
||||
/// - Returns `true` if other have merge into self.
|
||||
/// - Returns `false` if not able to merge.
|
||||
pub fn try_merge(&mut self, other: &Self) -> bool {
|
||||
if let Self::Texts { children, .. } = self {
|
||||
if let Self::Texts {
|
||||
children: other_children,
|
||||
..
|
||||
} = other
|
||||
{
|
||||
children.extend(other_children.clone());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
pub fn merge(&mut self, other: &Self) {
|
||||
self.children.extend(other.children.clone());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -307,92 +287,132 @@ impl Node {
|
|||
|
||||
impl RenderOnce for Paragraph {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
match self {
|
||||
Self::Texts { span, children } => {
|
||||
let mut text = String::new();
|
||||
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = vec![];
|
||||
let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
|
||||
let mut offset = 0;
|
||||
let span = self.span;
|
||||
let children = self.children;
|
||||
|
||||
for text_node in children.into_iter() {
|
||||
let text_len = text_node.text.len();
|
||||
let part = if text.len() == 0 {
|
||||
// trim start for first text
|
||||
text_node.text.trim_start()
|
||||
} else {
|
||||
text_node.text.as_str()
|
||||
};
|
||||
text.push_str(&part);
|
||||
let mut child_nodes: Vec<AnyElement> = vec![];
|
||||
|
||||
let mut node_highlights = vec![];
|
||||
for (range, style) in text_node.marks {
|
||||
let inner_range = (offset + range.start)..(offset + range.end);
|
||||
let mut text = String::new();
|
||||
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = vec![];
|
||||
let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
|
||||
let mut offset = 0;
|
||||
|
||||
let mut highlight = HighlightStyle::default();
|
||||
if style.bold {
|
||||
highlight.font_weight = Some(FontWeight::BOLD);
|
||||
}
|
||||
if style.italic {
|
||||
highlight.font_style = Some(FontStyle::Italic);
|
||||
}
|
||||
if style.strikethrough {
|
||||
highlight.strikethrough = Some(gpui::StrikethroughStyle {
|
||||
thickness: gpui::px(1.),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if style.code {
|
||||
highlight.background_color = Some(cx.theme().accent);
|
||||
fn inline_text(
|
||||
ix: usize,
|
||||
text: String,
|
||||
links: Vec<(Range<usize>, LinkMark)>,
|
||||
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
window: &Window,
|
||||
) -> AnyElement {
|
||||
let text_style = window.text_style();
|
||||
let styled_text =
|
||||
StyledText::new(text).with_default_highlights(&text_style, highlights);
|
||||
let link_ranges = links
|
||||
.iter()
|
||||
.map(|(range, _)| range.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
InteractiveText::new(ix, styled_text)
|
||||
.on_click(link_ranges, {
|
||||
let links = links.clone();
|
||||
move |ix, _, cx| {
|
||||
if let Some((_, link)) = &links.get(ix) {
|
||||
// Stop propagation to prevent the parent element from handling the event.
|
||||
//
|
||||
// For example the text in a checkbox label, click link need avoid toggle check state.
|
||||
cx.stop_propagation();
|
||||
cx.open_url(&link.url);
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
let mut ix = 0;
|
||||
for text_node in children.into_iter() {
|
||||
let text_len = text_node.text.len();
|
||||
text.push_str(&text_node.text);
|
||||
|
||||
if let Some(link_mark) = style.link {
|
||||
highlight.color = Some(cx.theme().link);
|
||||
highlight.underline = Some(gpui::UnderlineStyle {
|
||||
thickness: gpui::px(1.),
|
||||
..Default::default()
|
||||
});
|
||||
if let Some(image) = &text_node.image {
|
||||
if text.len() > 0 {
|
||||
child_nodes.push(inline_text(
|
||||
ix,
|
||||
text.clone(),
|
||||
links.clone(),
|
||||
highlights.clone(),
|
||||
window,
|
||||
));
|
||||
}
|
||||
child_nodes.push(
|
||||
img(image.url.clone())
|
||||
.id(ix)
|
||||
.object_fit(ObjectFit::Contain)
|
||||
.max_w(relative(1.))
|
||||
.when_some(image.width, |this, width| this.w(width))
|
||||
.when_some(image.link.clone(), |this, link| {
|
||||
let title = image.title();
|
||||
this.cursor_pointer()
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::new(title.clone()).build(window, cx)
|
||||
})
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.open_url(&link.url);
|
||||
})
|
||||
})
|
||||
.into_any_element(),
|
||||
);
|
||||
|
||||
links.push((inner_range.clone(), link_mark));
|
||||
}
|
||||
text.clear();
|
||||
links.clear();
|
||||
highlights.clear();
|
||||
offset = 0;
|
||||
} else {
|
||||
let mut node_highlights = vec![];
|
||||
for (range, style) in text_node.marks {
|
||||
let inner_range = (offset + range.start)..(offset + range.end);
|
||||
|
||||
node_highlights.push((inner_range, highlight));
|
||||
let mut highlight = HighlightStyle::default();
|
||||
if style.bold {
|
||||
highlight.font_weight = Some(FontWeight::BOLD);
|
||||
}
|
||||
if style.italic {
|
||||
highlight.font_style = Some(FontStyle::Italic);
|
||||
}
|
||||
if style.strikethrough {
|
||||
highlight.strikethrough = Some(gpui::StrikethroughStyle {
|
||||
thickness: gpui::px(1.),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if style.code {
|
||||
highlight.background_color = Some(cx.theme().accent);
|
||||
}
|
||||
|
||||
highlights = gpui::combine_highlights(highlights, node_highlights).collect();
|
||||
if let Some(link_mark) = style.link {
|
||||
highlight.color = Some(cx.theme().link);
|
||||
highlight.underline = Some(gpui::UnderlineStyle {
|
||||
thickness: gpui::px(1.),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
offset += text_len;
|
||||
links.push((inner_range.clone(), link_mark));
|
||||
}
|
||||
|
||||
node_highlights.push((inner_range, highlight));
|
||||
}
|
||||
|
||||
let text_style = window.text_style();
|
||||
let element_id: ElementId = span.unwrap_or_default().into();
|
||||
let styled_text =
|
||||
StyledText::new(text).with_default_highlights(&text_style, highlights);
|
||||
let link_ranges = links
|
||||
.iter()
|
||||
.map(|(range, _)| range.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
InteractiveText::new(element_id, styled_text)
|
||||
.on_click(link_ranges, {
|
||||
let links = links.clone();
|
||||
move |ix, _, cx| {
|
||||
if let Some((_, link)) = &links.get(ix) {
|
||||
// Stop propagation to prevent the parent element from handling the event.
|
||||
//
|
||||
// For example the text in a checkbox label, click link need avoid toggle check state.
|
||||
cx.stop_propagation();
|
||||
cx.open_url(&link.url);
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
highlights = gpui::combine_highlights(highlights, node_highlights).collect();
|
||||
offset += text_len;
|
||||
}
|
||||
Self::Image { image, .. } => img(image.url)
|
||||
.object_fit(ObjectFit::Contain)
|
||||
.max_w(relative(1.))
|
||||
.when_some(image.width, |this, width| this.w(width))
|
||||
.into_any_element(),
|
||||
ix += 1;
|
||||
}
|
||||
|
||||
if text.len() > 0 {
|
||||
// Add the last text node
|
||||
child_nodes.push(inline_text(ix, text, links, highlights, window));
|
||||
}
|
||||
|
||||
div().id(span.unwrap_or_default()).children(child_nodes)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -733,41 +753,42 @@ impl Node {
|
|||
|
||||
impl Paragraph {
|
||||
fn to_markdown(&self) -> String {
|
||||
let mut text = match self {
|
||||
Paragraph::Texts { children, .. } => children
|
||||
.iter()
|
||||
.map(|text_node| {
|
||||
let mut text = text_node.text.clone();
|
||||
for (range, style) in &text_node.marks {
|
||||
if style.bold {
|
||||
text = format!("**{}**", &text_node.text[range.clone()]);
|
||||
}
|
||||
if style.italic {
|
||||
text = format!("*{}*", &text_node.text[range.clone()]);
|
||||
}
|
||||
if style.strikethrough {
|
||||
text = format!("~~{}~~", &text_node.text[range.clone()]);
|
||||
}
|
||||
if style.code {
|
||||
text = format!("`{}`", &text_node.text[range.clone()]);
|
||||
}
|
||||
if let Some(link) = &style.link {
|
||||
text = format!("[{}]({})", &text_node.text[range.clone()], link.url);
|
||||
}
|
||||
let mut text = self
|
||||
.children
|
||||
.iter()
|
||||
.map(|text_node| {
|
||||
let mut text = text_node.text.clone();
|
||||
for (range, style) in &text_node.marks {
|
||||
if style.bold {
|
||||
text = format!("**{}**", &text_node.text[range.clone()]);
|
||||
}
|
||||
text
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(""),
|
||||
Paragraph::Image { image, .. } => {
|
||||
let alt = image.alt.clone().unwrap_or_default();
|
||||
let title = image
|
||||
.title
|
||||
.clone()
|
||||
.map_or(String::new(), |t| format!(" \"{}\"", t));
|
||||
format!("", alt, image.url, title)
|
||||
}
|
||||
};
|
||||
if style.italic {
|
||||
text = format!("*{}*", &text_node.text[range.clone()]);
|
||||
}
|
||||
if style.strikethrough {
|
||||
text = format!("~~{}~~", &text_node.text[range.clone()]);
|
||||
}
|
||||
if style.code {
|
||||
text = format!("`{}`", &text_node.text[range.clone()]);
|
||||
}
|
||||
if let Some(link) = &style.link {
|
||||
text = format!("[{}]({})", &text_node.text[range.clone()], link.url);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(image) = &text_node.image {
|
||||
let alt = image.alt.clone().unwrap_or_default();
|
||||
let title = image
|
||||
.title
|
||||
.clone()
|
||||
.map_or(String::new(), |t| format!(" \"{}\"", t));
|
||||
text.push_str(&format!("", alt, image.url, title))
|
||||
}
|
||||
|
||||
text
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
|
||||
text.push_str("\n\n");
|
||||
text
|
||||
|
|
|
|||
|
|
@ -416,6 +416,7 @@ fn parse_paragraph(
|
|||
));
|
||||
paragraph.push(element::TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: marks.clone(),
|
||||
});
|
||||
}
|
||||
|
|
@ -434,6 +435,7 @@ fn parse_paragraph(
|
|||
));
|
||||
paragraph.push(TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: marks.clone(),
|
||||
});
|
||||
}
|
||||
|
|
@ -452,6 +454,7 @@ fn parse_paragraph(
|
|||
));
|
||||
paragraph.push(TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: marks.clone(),
|
||||
});
|
||||
}
|
||||
|
|
@ -470,6 +473,7 @@ fn parse_paragraph(
|
|||
));
|
||||
paragraph.push(TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: marks.clone(),
|
||||
});
|
||||
}
|
||||
|
|
@ -494,6 +498,7 @@ fn parse_paragraph(
|
|||
));
|
||||
paragraph.push(TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: marks.clone(),
|
||||
});
|
||||
}
|
||||
|
|
@ -509,8 +514,9 @@ fn parse_paragraph(
|
|||
let title = attr_value(attrs, local_name!("title"));
|
||||
let (width, height) = attr_width_height(attrs);
|
||||
|
||||
paragraph.set_image(ImageNode {
|
||||
paragraph.push_image(ImageNode {
|
||||
url: src.into(),
|
||||
link: None,
|
||||
alt: alt.map(Into::into),
|
||||
width,
|
||||
height,
|
||||
|
|
@ -526,6 +532,7 @@ fn parse_paragraph(
|
|||
}
|
||||
paragraph.push(element::TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: marks.clone(),
|
||||
});
|
||||
}
|
||||
|
|
@ -538,6 +545,7 @@ fn parse_paragraph(
|
|||
}
|
||||
paragraph.push(TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: marks.clone(),
|
||||
});
|
||||
}
|
||||
|
|
@ -611,22 +619,21 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
|
|||
let title = attr_value(&attrs, local_name!("title"));
|
||||
let (width, height) = attr_width_height(&attrs);
|
||||
|
||||
let image = Paragraph::Image {
|
||||
span: None,
|
||||
image: ImageNode {
|
||||
url: src.into(),
|
||||
title: title.map(Into::into),
|
||||
alt: alt.map(Into::into),
|
||||
width,
|
||||
height,
|
||||
},
|
||||
};
|
||||
let mut paragraph = Paragraph::default();
|
||||
paragraph.push_image(ImageNode {
|
||||
url: src.into(),
|
||||
link: None,
|
||||
title: title.map(Into::into),
|
||||
alt: alt.map(Into::into),
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
if children.len() > 0 {
|
||||
children.push(element::Node::Paragraph(image));
|
||||
children.push(element::Node::Paragraph(paragraph));
|
||||
Some(element::Node::Root { children })
|
||||
} else {
|
||||
Some(element::Node::Paragraph(image))
|
||||
Some(element::Node::Paragraph(paragraph))
|
||||
}
|
||||
}
|
||||
local_name!("ul") | local_name!("ol") => {
|
||||
|
|
@ -647,9 +654,8 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<element::Nod
|
|||
// If last child is paragraph, merge child
|
||||
if let Some(last_child) = children.last_mut() {
|
||||
if let element::Node::Paragraph(last_paragraph) = last_child {
|
||||
if last_paragraph.try_merge(&child_paragraph) {
|
||||
continue;
|
||||
};
|
||||
last_paragraph.merge(&child_paragraph);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -774,7 +780,7 @@ fn consume_paragraph(children: &mut Vec<element::Node>, paragraph: &mut Paragrap
|
|||
mod tests {
|
||||
use gpui::{px, relative};
|
||||
|
||||
use crate::text::element::{Node, Paragraph};
|
||||
use crate::text::element::{Node, Paragraph, TextNode};
|
||||
|
||||
use super::trim_text;
|
||||
|
||||
|
|
@ -860,15 +866,20 @@ mod tests {
|
|||
let node = super::parse_html(html).unwrap();
|
||||
assert_eq!(
|
||||
node,
|
||||
Node::Paragraph(Paragraph::Image {
|
||||
Node::Paragraph(Paragraph {
|
||||
span: None,
|
||||
image: super::ImageNode {
|
||||
url: "https://example.com/image.png".to_string().into(),
|
||||
alt: Some("Example".to_string().into()),
|
||||
width: Some(px(100.).into()),
|
||||
height: Some(px(200.).into()),
|
||||
title: Some("Example Image".to_string().into())
|
||||
}
|
||||
children: vec![TextNode {
|
||||
text: String::new(),
|
||||
marks: vec![],
|
||||
image: Some(super::ImageNode {
|
||||
url: "https://example.com/image.png".to_string().into(),
|
||||
alt: Some("Example".to_string().into()),
|
||||
width: Some(px(100.).into()),
|
||||
height: Some(px(200.).into()),
|
||||
title: Some("Example Image".to_string().into()),
|
||||
..Default::default()
|
||||
}),
|
||||
}],
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -876,15 +887,20 @@ mod tests {
|
|||
let node = super::parse_html(html).unwrap();
|
||||
assert_eq!(
|
||||
node,
|
||||
Node::Paragraph(Paragraph::Image {
|
||||
Node::Paragraph(Paragraph {
|
||||
span: None,
|
||||
image: super::ImageNode {
|
||||
url: "https://example.com/image.png".to_string().into(),
|
||||
alt: Some("Example".to_string().into()),
|
||||
width: Some(relative(0.8)),
|
||||
height: None,
|
||||
title: Some("Example Image".to_string().into())
|
||||
}
|
||||
children: vec![TextNode {
|
||||
text: String::new(),
|
||||
marks: vec![],
|
||||
image: Some(super::ImageNode {
|
||||
url: "https://example.com/image.png".to_string().into(),
|
||||
alt: Some("Example".to_string().into()),
|
||||
width: Some(relative(0.8)),
|
||||
height: None,
|
||||
title: Some("Example Image".to_string().into()),
|
||||
..Default::default()
|
||||
}),
|
||||
}],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
}
|
||||
paragraph.push(element::TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: vec![(
|
||||
0..text.len(),
|
||||
InlineTextStyle {
|
||||
|
|
@ -246,6 +247,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
}
|
||||
paragraph.push(element::TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: vec![(
|
||||
0..text.len(),
|
||||
InlineTextStyle {
|
||||
|
|
@ -262,6 +264,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
}
|
||||
paragraph.push(element::TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: vec![(
|
||||
0..text.len(),
|
||||
InlineTextStyle {
|
||||
|
|
@ -275,6 +278,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
text = val.value.clone();
|
||||
paragraph.push(element::TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: vec![(
|
||||
0..text.len(),
|
||||
InlineTextStyle {
|
||||
|
|
@ -285,26 +289,36 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
});
|
||||
}
|
||||
Node::Link(val) => {
|
||||
let link_mark = Some(LinkMark {
|
||||
url: val.url.clone().into(),
|
||||
title: val.title.clone().map(|s| s.into()),
|
||||
});
|
||||
|
||||
let mut child_paragraph = Paragraph::default();
|
||||
for child in val.children.iter() {
|
||||
text.push_str(&parse_paragraph(&mut child_paragraph, &child));
|
||||
}
|
||||
paragraph.push(element::TextNode {
|
||||
text: text.clone(),
|
||||
marks: vec![(
|
||||
0..text.len(),
|
||||
|
||||
// FIXME: GPUI InteractiveText does not support inline images yet.
|
||||
// So here we push images to the paragraph directly.
|
||||
for child in child_paragraph.children.iter_mut() {
|
||||
if let Some(image) = child.image.as_mut() {
|
||||
image.link = link_mark.clone();
|
||||
}
|
||||
|
||||
child.marks.push((
|
||||
0..child.text.len(),
|
||||
InlineTextStyle {
|
||||
link: Some(LinkMark {
|
||||
url: val.url.clone().into(),
|
||||
title: val.title.clone().map(|s| s.into()),
|
||||
}),
|
||||
link: link_mark.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)],
|
||||
});
|
||||
));
|
||||
}
|
||||
|
||||
paragraph.merge(&child_paragraph);
|
||||
}
|
||||
Node::Image(raw) => {
|
||||
paragraph.set_image(ImageNode {
|
||||
paragraph.push_image(ImageNode {
|
||||
url: raw.url.clone().into(),
|
||||
title: raw.title.clone().map(|t| t.into()),
|
||||
alt: Some(raw.alt.clone().into()),
|
||||
|
|
@ -315,6 +329,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
text = raw.value.clone();
|
||||
paragraph.push(element::TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: vec![(
|
||||
0..text.len(),
|
||||
InlineTextStyle {
|
||||
|
|
@ -328,6 +343,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
text = raw.value.clone();
|
||||
paragraph.push(element::TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: vec![(0..text.len(), InlineTextStyle::default())],
|
||||
});
|
||||
}
|
||||
|
|
@ -337,6 +353,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
text = "\n".to_owned();
|
||||
paragraph.push(element::TextNode {
|
||||
text: text.clone(),
|
||||
image: None,
|
||||
marks: vec![(0..text.len(), InlineTextStyle::default())],
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue