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**!
|
# 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 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.
|
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, img, prelude::FluentBuilder as _, px, relative, rems, AnyElement, App, DefiniteLength,
|
||||||
Div, ElementId, FontStyle, FontWeight, Half, HighlightStyle, InteractiveElement as _,
|
Div, ElementId, FontStyle, FontWeight, Half, HighlightStyle, InteractiveElement as _,
|
||||||
InteractiveText, IntoElement, Length, ObjectFit, ParentElement, Rems, RenderOnce, SharedString,
|
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 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};
|
use super::{utils::list_item_prefix, TextViewStyle};
|
||||||
|
|
||||||
|
|
@ -44,12 +47,22 @@ impl From<Span> for ElementId {
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct ImageNode {
|
pub struct ImageNode {
|
||||||
pub url: SharedUri,
|
pub url: SharedUri,
|
||||||
|
pub link: Option<LinkMark>,
|
||||||
pub title: Option<SharedString>,
|
pub title: Option<SharedString>,
|
||||||
pub alt: Option<SharedString>,
|
pub alt: Option<SharedString>,
|
||||||
pub width: Option<DefiniteLength>,
|
pub width: Option<DefiniteLength>,
|
||||||
pub height: 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 {
|
impl PartialEq for ImageNode {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.url == other.url && self.title == other.title && self.alt == other.alt
|
self.url == other.url && self.title == other.title && self.alt == other.alt
|
||||||
|
|
@ -60,37 +73,24 @@ impl PartialEq for ImageNode {
|
||||||
pub struct TextNode {
|
pub struct TextNode {
|
||||||
/// The text content.
|
/// The text content.
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
pub image: Option<ImageNode>,
|
||||||
/// The text styles, each tuple contains the range of the text and the style.
|
/// The text styles, each tuple contains the range of the text and the style.
|
||||||
pub marks: Vec<(Range<usize>, InlineTextStyle)>,
|
pub marks: Vec<(Range<usize>, InlineTextStyle)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, IntoElement)]
|
#[derive(Debug, Default, Clone, PartialEq, IntoElement)]
|
||||||
pub enum Paragraph {
|
pub struct Paragraph {
|
||||||
Texts {
|
pub(super) span: Option<Span>,
|
||||||
span: Option<Span>,
|
pub(super) children: Vec<TextNode>,
|
||||||
children: Vec<TextNode>,
|
|
||||||
},
|
|
||||||
Image {
|
|
||||||
span: Option<Span>,
|
|
||||||
image: ImageNode,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Paragraph {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Texts {
|
|
||||||
span: None,
|
|
||||||
children: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for Paragraph {
|
impl From<String> for Paragraph {
|
||||||
fn from(value: String) -> Self {
|
fn from(value: String) -> Self {
|
||||||
Self::Texts {
|
Self {
|
||||||
span: None,
|
span: None,
|
||||||
children: vec![TextNode {
|
children: vec![TextNode {
|
||||||
text: value.clone(),
|
text: value.clone(),
|
||||||
|
image: None,
|
||||||
marks: vec![],
|
marks: vec![],
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
|
|
@ -141,80 +141,60 @@ pub struct TableCell {
|
||||||
|
|
||||||
impl Paragraph {
|
impl Paragraph {
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
match self {
|
self.span = None;
|
||||||
Self::Texts { children, .. } => children.clear(),
|
self.children.clear();
|
||||||
Self::Image { .. } => *self = Self::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_image(&self) -> bool {
|
pub fn is_image(&self) -> bool {
|
||||||
matches!(self, Self::Image { .. })
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_span(&mut self, span: Span) {
|
pub fn set_span(&mut self, span: Span) {
|
||||||
match self {
|
self.span = Some(span);
|
||||||
Self::Texts { span: s, .. } => *s = Some(span),
|
|
||||||
Self::Image { span: s, .. } => *s = Some(span),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_str(&mut self, text: &str) {
|
pub fn push_str(&mut self, text: &str) {
|
||||||
if let Self::Texts { children, .. } = self {
|
self.children.push(TextNode {
|
||||||
children.push(TextNode {
|
text: text.to_string(),
|
||||||
text: text.to_string(),
|
image: None,
|
||||||
marks: vec![(0..text.len(), InlineTextStyle::default())],
|
marks: vec![(0..text.len(), InlineTextStyle::default())],
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, text: TextNode) {
|
pub fn push(&mut self, text: TextNode) {
|
||||||
if let Self::Texts { children, .. } = self {
|
self.children.push(text);
|
||||||
children.push(text);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_image(&mut self, image: ImageNode) {
|
pub fn push_image(&mut self, image: ImageNode) {
|
||||||
*self = Self::Image { span: None, image };
|
self.children.push(TextNode {
|
||||||
|
text: String::new(),
|
||||||
|
image: Some(image),
|
||||||
|
marks: vec![],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
match self {
|
self.children.is_empty()
|
||||||
Self::Texts { .. } => self.text_len() == 0,
|
|| self
|
||||||
Self::Image { .. } => false,
|
.children
|
||||||
}
|
.iter()
|
||||||
|
.all(|node| node.text.is_empty() && node.image.is_none())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return length of children text.
|
/// Return length of children text.
|
||||||
pub fn text_len(&self) -> usize {
|
pub fn text_len(&self) -> usize {
|
||||||
match self {
|
self.children
|
||||||
Self::Texts { children, .. } => {
|
.iter()
|
||||||
let mut len = 0;
|
.map(|node| node.text.len())
|
||||||
for text_node in children.iter() {
|
.sum::<usize>()
|
||||||
len = text_node.text.len().max(len);
|
|
||||||
}
|
|
||||||
len
|
|
||||||
}
|
|
||||||
Self::Image { .. } => 1,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Try to merge two paragraphs, if they are both text elements.
|
/// Try to merge two paragraphs, if they are both text elements.
|
||||||
///
|
///
|
||||||
/// - Returns `true` if other have merge into self.
|
/// - Returns `true` if other have merge into self.
|
||||||
/// - Returns `false` if not able to merge.
|
/// - Returns `false` if not able to merge.
|
||||||
pub fn try_merge(&mut self, other: &Self) -> bool {
|
pub fn merge(&mut self, other: &Self) {
|
||||||
if let Self::Texts { children, .. } = self {
|
self.children.extend(other.children.clone());
|
||||||
if let Self::Texts {
|
|
||||||
children: other_children,
|
|
||||||
..
|
|
||||||
} = other
|
|
||||||
{
|
|
||||||
children.extend(other_children.clone());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -307,92 +287,132 @@ impl Node {
|
||||||
|
|
||||||
impl RenderOnce for Paragraph {
|
impl RenderOnce for Paragraph {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
match self {
|
let span = self.span;
|
||||||
Self::Texts { span, children } => {
|
let children = self.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;
|
|
||||||
|
|
||||||
for text_node in children.into_iter() {
|
let mut child_nodes: Vec<AnyElement> = vec![];
|
||||||
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 node_highlights = vec![];
|
let mut text = String::new();
|
||||||
for (range, style) in text_node.marks {
|
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = vec![];
|
||||||
let inner_range = (offset + range.start)..(offset + range.end);
|
let mut links: Vec<(Range<usize>, LinkMark)> = vec![];
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
let mut highlight = HighlightStyle::default();
|
fn inline_text(
|
||||||
if style.bold {
|
ix: usize,
|
||||||
highlight.font_weight = Some(FontWeight::BOLD);
|
text: String,
|
||||||
}
|
links: Vec<(Range<usize>, LinkMark)>,
|
||||||
if style.italic {
|
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||||
highlight.font_style = Some(FontStyle::Italic);
|
window: &Window,
|
||||||
}
|
) -> AnyElement {
|
||||||
if style.strikethrough {
|
let text_style = window.text_style();
|
||||||
highlight.strikethrough = Some(gpui::StrikethroughStyle {
|
let styled_text =
|
||||||
thickness: gpui::px(1.),
|
StyledText::new(text).with_default_highlights(&text_style, highlights);
|
||||||
..Default::default()
|
let link_ranges = links
|
||||||
});
|
.iter()
|
||||||
}
|
.map(|(range, _)| range.clone())
|
||||||
if style.code {
|
.collect::<Vec<_>>();
|
||||||
highlight.background_color = Some(cx.theme().accent);
|
|
||||||
|
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 {
|
if let Some(image) = &text_node.image {
|
||||||
highlight.color = Some(cx.theme().link);
|
if text.len() > 0 {
|
||||||
highlight.underline = Some(gpui::UnderlineStyle {
|
child_nodes.push(inline_text(
|
||||||
thickness: gpui::px(1.),
|
ix,
|
||||||
..Default::default()
|
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();
|
highlights = gpui::combine_highlights(highlights, node_highlights).collect();
|
||||||
let element_id: ElementId = span.unwrap_or_default().into();
|
offset += text_len;
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
Self::Image { image, .. } => img(image.url)
|
ix += 1;
|
||||||
.object_fit(ObjectFit::Contain)
|
|
||||||
.max_w(relative(1.))
|
|
||||||
.when_some(image.width, |this, width| this.w(width))
|
|
||||||
.into_any_element(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
impl Paragraph {
|
||||||
fn to_markdown(&self) -> String {
|
fn to_markdown(&self) -> String {
|
||||||
let mut text = match self {
|
let mut text = self
|
||||||
Paragraph::Texts { children, .. } => children
|
.children
|
||||||
.iter()
|
.iter()
|
||||||
.map(|text_node| {
|
.map(|text_node| {
|
||||||
let mut text = text_node.text.clone();
|
let mut text = text_node.text.clone();
|
||||||
for (range, style) in &text_node.marks {
|
for (range, style) in &text_node.marks {
|
||||||
if style.bold {
|
if style.bold {
|
||||||
text = format!("**{}**", &text_node.text[range.clone()]);
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
text
|
if style.italic {
|
||||||
})
|
text = format!("*{}*", &text_node.text[range.clone()]);
|
||||||
.collect::<Vec<_>>()
|
}
|
||||||
.join(""),
|
if style.strikethrough {
|
||||||
Paragraph::Image { image, .. } => {
|
text = format!("~~{}~~", &text_node.text[range.clone()]);
|
||||||
let alt = image.alt.clone().unwrap_or_default();
|
}
|
||||||
let title = image
|
if style.code {
|
||||||
.title
|
text = format!("`{}`", &text_node.text[range.clone()]);
|
||||||
.clone()
|
}
|
||||||
.map_or(String::new(), |t| format!(" \"{}\"", t));
|
if let Some(link) = &style.link {
|
||||||
format!("", alt, image.url, title)
|
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.push_str("\n\n");
|
||||||
text
|
text
|
||||||
|
|
|
||||||
|
|
@ -416,6 +416,7 @@ fn parse_paragraph(
|
||||||
));
|
));
|
||||||
paragraph.push(element::TextNode {
|
paragraph.push(element::TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: marks.clone(),
|
marks: marks.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -434,6 +435,7 @@ fn parse_paragraph(
|
||||||
));
|
));
|
||||||
paragraph.push(TextNode {
|
paragraph.push(TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: marks.clone(),
|
marks: marks.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -452,6 +454,7 @@ fn parse_paragraph(
|
||||||
));
|
));
|
||||||
paragraph.push(TextNode {
|
paragraph.push(TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: marks.clone(),
|
marks: marks.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -470,6 +473,7 @@ fn parse_paragraph(
|
||||||
));
|
));
|
||||||
paragraph.push(TextNode {
|
paragraph.push(TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: marks.clone(),
|
marks: marks.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -494,6 +498,7 @@ fn parse_paragraph(
|
||||||
));
|
));
|
||||||
paragraph.push(TextNode {
|
paragraph.push(TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: marks.clone(),
|
marks: marks.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -509,8 +514,9 @@ fn parse_paragraph(
|
||||||
let title = attr_value(attrs, local_name!("title"));
|
let title = attr_value(attrs, local_name!("title"));
|
||||||
let (width, height) = attr_width_height(attrs);
|
let (width, height) = attr_width_height(attrs);
|
||||||
|
|
||||||
paragraph.set_image(ImageNode {
|
paragraph.push_image(ImageNode {
|
||||||
url: src.into(),
|
url: src.into(),
|
||||||
|
link: None,
|
||||||
alt: alt.map(Into::into),
|
alt: alt.map(Into::into),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|
@ -526,6 +532,7 @@ fn parse_paragraph(
|
||||||
}
|
}
|
||||||
paragraph.push(element::TextNode {
|
paragraph.push(element::TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: marks.clone(),
|
marks: marks.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -538,6 +545,7 @@ fn parse_paragraph(
|
||||||
}
|
}
|
||||||
paragraph.push(TextNode {
|
paragraph.push(TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: marks.clone(),
|
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 title = attr_value(&attrs, local_name!("title"));
|
||||||
let (width, height) = attr_width_height(&attrs);
|
let (width, height) = attr_width_height(&attrs);
|
||||||
|
|
||||||
let image = Paragraph::Image {
|
let mut paragraph = Paragraph::default();
|
||||||
span: None,
|
paragraph.push_image(ImageNode {
|
||||||
image: ImageNode {
|
url: src.into(),
|
||||||
url: src.into(),
|
link: None,
|
||||||
title: title.map(Into::into),
|
title: title.map(Into::into),
|
||||||
alt: alt.map(Into::into),
|
alt: alt.map(Into::into),
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
},
|
});
|
||||||
};
|
|
||||||
|
|
||||||
if children.len() > 0 {
|
if children.len() > 0 {
|
||||||
children.push(element::Node::Paragraph(image));
|
children.push(element::Node::Paragraph(paragraph));
|
||||||
Some(element::Node::Root { children })
|
Some(element::Node::Root { children })
|
||||||
} else {
|
} else {
|
||||||
Some(element::Node::Paragraph(image))
|
Some(element::Node::Paragraph(paragraph))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
local_name!("ul") | local_name!("ol") => {
|
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 last child is paragraph, merge child
|
||||||
if let Some(last_child) = children.last_mut() {
|
if let Some(last_child) = children.last_mut() {
|
||||||
if let element::Node::Paragraph(last_paragraph) = last_child {
|
if let element::Node::Paragraph(last_paragraph) = last_child {
|
||||||
if last_paragraph.try_merge(&child_paragraph) {
|
last_paragraph.merge(&child_paragraph);
|
||||||
continue;
|
continue;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -774,7 +780,7 @@ fn consume_paragraph(children: &mut Vec<element::Node>, paragraph: &mut Paragrap
|
||||||
mod tests {
|
mod tests {
|
||||||
use gpui::{px, relative};
|
use gpui::{px, relative};
|
||||||
|
|
||||||
use crate::text::element::{Node, Paragraph};
|
use crate::text::element::{Node, Paragraph, TextNode};
|
||||||
|
|
||||||
use super::trim_text;
|
use super::trim_text;
|
||||||
|
|
||||||
|
|
@ -860,15 +866,20 @@ mod tests {
|
||||||
let node = super::parse_html(html).unwrap();
|
let node = super::parse_html(html).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
node,
|
node,
|
||||||
Node::Paragraph(Paragraph::Image {
|
Node::Paragraph(Paragraph {
|
||||||
span: None,
|
span: None,
|
||||||
image: super::ImageNode {
|
children: vec![TextNode {
|
||||||
url: "https://example.com/image.png".to_string().into(),
|
text: String::new(),
|
||||||
alt: Some("Example".to_string().into()),
|
marks: vec![],
|
||||||
width: Some(px(100.).into()),
|
image: Some(super::ImageNode {
|
||||||
height: Some(px(200.).into()),
|
url: "https://example.com/image.png".to_string().into(),
|
||||||
title: Some("Example Image".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();
|
let node = super::parse_html(html).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
node,
|
node,
|
||||||
Node::Paragraph(Paragraph::Image {
|
Node::Paragraph(Paragraph {
|
||||||
span: None,
|
span: None,
|
||||||
image: super::ImageNode {
|
children: vec![TextNode {
|
||||||
url: "https://example.com/image.png".to_string().into(),
|
text: String::new(),
|
||||||
alt: Some("Example".to_string().into()),
|
marks: vec![],
|
||||||
width: Some(relative(0.8)),
|
image: Some(super::ImageNode {
|
||||||
height: None,
|
url: "https://example.com/image.png".to_string().into(),
|
||||||
title: Some("Example Image".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 {
|
paragraph.push(element::TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: vec![(
|
marks: vec![(
|
||||||
0..text.len(),
|
0..text.len(),
|
||||||
InlineTextStyle {
|
InlineTextStyle {
|
||||||
|
|
@ -246,6 +247,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
||||||
}
|
}
|
||||||
paragraph.push(element::TextNode {
|
paragraph.push(element::TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: vec![(
|
marks: vec![(
|
||||||
0..text.len(),
|
0..text.len(),
|
||||||
InlineTextStyle {
|
InlineTextStyle {
|
||||||
|
|
@ -262,6 +264,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
||||||
}
|
}
|
||||||
paragraph.push(element::TextNode {
|
paragraph.push(element::TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: vec![(
|
marks: vec![(
|
||||||
0..text.len(),
|
0..text.len(),
|
||||||
InlineTextStyle {
|
InlineTextStyle {
|
||||||
|
|
@ -275,6 +278,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
||||||
text = val.value.clone();
|
text = val.value.clone();
|
||||||
paragraph.push(element::TextNode {
|
paragraph.push(element::TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: vec![(
|
marks: vec![(
|
||||||
0..text.len(),
|
0..text.len(),
|
||||||
InlineTextStyle {
|
InlineTextStyle {
|
||||||
|
|
@ -285,26 +289,36 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Node::Link(val) => {
|
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();
|
let mut child_paragraph = Paragraph::default();
|
||||||
for child in val.children.iter() {
|
for child in val.children.iter() {
|
||||||
text.push_str(&parse_paragraph(&mut child_paragraph, &child));
|
text.push_str(&parse_paragraph(&mut child_paragraph, &child));
|
||||||
}
|
}
|
||||||
paragraph.push(element::TextNode {
|
|
||||||
text: text.clone(),
|
// FIXME: GPUI InteractiveText does not support inline images yet.
|
||||||
marks: vec![(
|
// So here we push images to the paragraph directly.
|
||||||
0..text.len(),
|
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 {
|
InlineTextStyle {
|
||||||
link: Some(LinkMark {
|
link: link_mark.clone(),
|
||||||
url: val.url.clone().into(),
|
|
||||||
title: val.title.clone().map(|s| s.into()),
|
|
||||||
}),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)],
|
));
|
||||||
});
|
}
|
||||||
|
|
||||||
|
paragraph.merge(&child_paragraph);
|
||||||
}
|
}
|
||||||
Node::Image(raw) => {
|
Node::Image(raw) => {
|
||||||
paragraph.set_image(ImageNode {
|
paragraph.push_image(ImageNode {
|
||||||
url: raw.url.clone().into(),
|
url: raw.url.clone().into(),
|
||||||
title: raw.title.clone().map(|t| t.into()),
|
title: raw.title.clone().map(|t| t.into()),
|
||||||
alt: Some(raw.alt.clone().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();
|
text = raw.value.clone();
|
||||||
paragraph.push(element::TextNode {
|
paragraph.push(element::TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: vec![(
|
marks: vec![(
|
||||||
0..text.len(),
|
0..text.len(),
|
||||||
InlineTextStyle {
|
InlineTextStyle {
|
||||||
|
|
@ -328,6 +343,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
||||||
text = raw.value.clone();
|
text = raw.value.clone();
|
||||||
paragraph.push(element::TextNode {
|
paragraph.push(element::TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: vec![(0..text.len(), InlineTextStyle::default())],
|
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();
|
text = "\n".to_owned();
|
||||||
paragraph.push(element::TextNode {
|
paragraph.push(element::TextNode {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
|
image: None,
|
||||||
marks: vec![(0..text.len(), InlineTextStyle::default())],
|
marks: vec![(0..text.len(), InlineTextStyle::default())],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue