text_view: Fix some case may lose spaces, <br> in HTML and Markdown render. (#667)
This commit is contained in:
parent
ffa848fb26
commit
3ba63915ce
8 changed files with 366 additions and 91 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -527,7 +527,7 @@ dependencies = [
|
|||
"bitflags 2.6.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
|
|
@ -2546,6 +2546,7 @@ dependencies = [
|
|||
"gpui",
|
||||
"html5ever 0.27.0",
|
||||
"image",
|
||||
"indoc",
|
||||
"itertools 0.13.0",
|
||||
"markdown",
|
||||
"markup5ever_rcdom",
|
||||
|
|
@ -3025,6 +3026,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.3"
|
||||
|
|
@ -3915,7 +3922,7 @@ version = "0.7.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.3.1",
|
||||
"proc-macro-crate 3.2.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.96",
|
||||
|
|
|
|||
|
|
@ -72,8 +72,7 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Cell</strong> 1</td>
|
||||
<td>Cell 2</td>
|
||||
<td span="2">This <strong>Cell</strong> have 2 span</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cell 3</td>
|
||||
|
|
@ -94,21 +93,18 @@
|
|||
(A Tesla Model X on display at the June 2024 Shanghai new energy
|
||||
vehicle show. Image credit: CnEVPost)
|
||||
</p>
|
||||
<h3>SVG</h3>
|
||||
<img
|
||||
src="https://miro.medium.com/v2/resize:fit:1400/format:webp/1*-Y9ozbNWSViiCmal1TT32w.jpeg"
|
||||
width="100%"
|
||||
/>
|
||||
Text before the image.
|
||||
<img
|
||||
src="https://www.rust-lang.org/logos/rust-logo-blk.svg"
|
||||
alt="Rust"
|
||||
src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*u4La03Nh6E4zIc9-.jpeg"
|
||||
width="100%"
|
||||
/>
|
||||
Text after the image.
|
||||
<img
|
||||
src="https://www.rust-lang.org/logos/rust-logo-blk.svg"
|
||||
alt="Rust"
|
||||
src="https://miro.medium.com/v2/resize:fit:1400/format:webp/0*Q_JiltniByWLWoUv"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ This is an additional demonstration paragraph in English demonstrating more cont
|
|||
|
||||

|
||||
|
||||
---
|
||||
|
||||
这是一个中文演示段落,用于展示更多的 [Markdown GFM](https://github.github.com/gfm/) 内容。您可以在此尝试使用使用**粗体**、*斜体*和`代码`等样式。これは日本語のデモ段落です。Markdown の多言語サポートを示すためのテキストが含まれています。例えば、、**ボールド**、_イタリック_、および`コード`のスタイルなどを試すことができます。
|
||||
|
||||
---
|
||||
|
||||
## Heading for [Links](https://www.google.com)
|
||||
|
||||
Here is a link to [Google](https://www.google.com), and another to [Rust](https://www.rust-lang.org).
|
||||
|
|
@ -25,6 +25,50 @@ Here is a link to [Google](https://www.google.com), and another to [Rust](https:
|
|||
|
||||

|
||||
|
||||
### Table
|
||||
|
||||
| Header 1 | Centered | Header 3 | Align Right |
|
||||
| -------- | :------: | ------------------------------------ | ----------: |
|
||||
| Cell 0 | Cell 1 | This is a long cell with line break. | Cell 3 |
|
||||
| Row 2 | Row 2 | Row 2<br>[Link](https://github.com) | Row 2 |
|
||||
| Row 3 | **Bold** | Row 3 | Row 3 |
|
||||
|
||||
#### Lists
|
||||
|
||||
##### Bulleted List
|
||||
|
||||
- Bullet 1
|
||||
- Bullet 2
|
||||
- Bullet 2.1
|
||||
- Bullet 2.1.1
|
||||
- Bullet 2.1.1.1
|
||||
- Bullet 2.1.2
|
||||
- Bullet 2.2
|
||||
- Bullet 3
|
||||
|
||||
##### Numbered List
|
||||
|
||||
1. Numbered item 1
|
||||
1. Numbered item 1.1
|
||||
1. Numbered item 1.1.1
|
||||
1. Numbered item 1.2
|
||||
2. Numbered item 2
|
||||
3. Numbered item 3
|
||||
|
||||
##### To-Do List
|
||||
|
||||
- [x] Task 1
|
||||
- [ ] Task 2
|
||||
- [ ] Task 3
|
||||
|
||||
#### Heading for Code
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
println!("Hello, World!");
|
||||
}
|
||||
```
|
||||
|
||||
### HTML
|
||||
|
||||
#### Paragraph and Text
|
||||
|
|
@ -77,50 +121,6 @@ Here is a link to [Google](https://www.google.com), and another to [Rust](https:
|
|||
<img src="https://www.rust-lang.org/logos/rust-logo-blk.svg" alt="Rust" width="100%" />
|
||||
<img src="https://www.rust-lang.org/logos/rust-logo-blk.svg" alt="Rust" style="width:100%" />
|
||||
|
||||
### Table
|
||||
|
||||
| Header 1 | Centered | Header 3 | Align Right |
|
||||
| -------- | :------: | ------------------------------------ | ----------: |
|
||||
| Cell 0 | Cell 1 | This is a long cell with line break. | Cell 3 |
|
||||
| Row 2 | Row 2 | Row 2<br>[Link](https://github.com) | Row 2 |
|
||||
| Row 3 | **Bold** | Row 3 | Row 3 |
|
||||
|
||||
#### Lists
|
||||
|
||||
##### Bulleted List
|
||||
|
||||
- Bullet 1
|
||||
- Bullet 2
|
||||
- Bullet 2.1
|
||||
- Bullet 2.1.1
|
||||
- Bullet 2.1.1.1
|
||||
- Bullet 2.1.2
|
||||
- Bullet 2.2
|
||||
- Bullet 3
|
||||
|
||||
##### Numbered List
|
||||
|
||||
1. Numbered item 1
|
||||
1. Numbered item 1.1
|
||||
1. Numbered item 1.1.1
|
||||
1. Numbered item 1.2
|
||||
2. Numbered item 2
|
||||
3. Numbered item 3
|
||||
|
||||
##### To-Do List
|
||||
|
||||
- [x] Task 1
|
||||
- [ ] Task 2
|
||||
- [ ] Task 3
|
||||
|
||||
#### Heading for Code
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
println!("Hello, World!");
|
||||
}
|
||||
```
|
||||
|
||||
## Unsupported
|
||||
|
||||
### HTML
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ once_cell = "1.19.0"
|
|||
paste = "1"
|
||||
regex = "1"
|
||||
resvg = { version = "0.44.0", default-features = false, features = [
|
||||
"system-fonts",
|
||||
"text",
|
||||
"system-fonts",
|
||||
"text",
|
||||
] }
|
||||
rust-embed.workspace = true
|
||||
rust-i18n = "3"
|
||||
|
|
@ -34,20 +34,22 @@ smallvec = "1.13.2"
|
|||
smol = "1"
|
||||
unicode-segmentation = "1.12.0"
|
||||
usvg = { version = "0.44.0", default-features = false, features = [
|
||||
"system-fonts",
|
||||
"text",
|
||||
"system-fonts",
|
||||
"text",
|
||||
] }
|
||||
uuid = "1.10"
|
||||
wry = { version = "0.48.0", optional = true }
|
||||
# Markdown Parser
|
||||
markdown = "1.0.0-alpha.22"
|
||||
# HTML Parser
|
||||
html5ever = "0.27"
|
||||
html5ever = "0.27"
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
minify-html = "0.15.0"
|
||||
|
||||
# Calendar
|
||||
chrono = "0.4.38"
|
||||
|
||||
[dev-dependencies]
|
||||
indoc = "2"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -229,22 +229,27 @@ pub enum Node {
|
|||
lang: Option<SharedString>,
|
||||
},
|
||||
Table(Table),
|
||||
// <br>
|
||||
Break,
|
||||
Break {
|
||||
html: bool,
|
||||
},
|
||||
Divider,
|
||||
Ignore,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn is_ignore(&self) -> bool {
|
||||
pub(super) fn is_ignore(&self) -> bool {
|
||||
matches!(self, Self::Ignore)
|
||||
}
|
||||
|
||||
fn is_list_item(&self) -> bool {
|
||||
pub(super) fn is_list_item(&self) -> bool {
|
||||
matches!(self, Self::ListItem { .. })
|
||||
}
|
||||
|
||||
pub(super) fn is_break(&self) -> bool {
|
||||
matches!(self, Self::Break { .. })
|
||||
}
|
||||
|
||||
/// Combine all children, omitting the empt parent nodes.
|
||||
pub(super) fn compact(&self) -> Node {
|
||||
match self {
|
||||
|
|
@ -276,7 +281,14 @@ impl RenderOnce for Paragraph {
|
|||
|
||||
for text_node in children.into_iter() {
|
||||
let text_len = text_node.text.len();
|
||||
text.push_str(&text_node.text);
|
||||
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![];
|
||||
for (range, style) in text_node.marks {
|
||||
|
|
@ -581,7 +593,6 @@ impl Node {
|
|||
.border_l_3()
|
||||
.border_color(cx.theme().secondary_active)
|
||||
.px_4()
|
||||
.py_1()
|
||||
.child(children)
|
||||
.into_any_element(),
|
||||
Node::List { children, ordered } => v_flex()
|
||||
|
|
@ -628,7 +639,7 @@ impl Node {
|
|||
.h(px(2.))
|
||||
.mb(mb)
|
||||
.into_any_element(),
|
||||
Node::Break => div().into_any_element(),
|
||||
Node::Break { .. } => div().into_any_element(),
|
||||
Node::Ignore => div().into_any_element(),
|
||||
_ => {
|
||||
if cfg!(debug_assertions) {
|
||||
|
|
@ -640,3 +651,165 @@ 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);
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
text.push_str("\n\n");
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// Converts the node to markdown format.
|
||||
///
|
||||
/// This is used to generate markdown for test.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn to_markdown(&self) -> String {
|
||||
match self {
|
||||
Node::Root { children } => children
|
||||
.iter()
|
||||
.map(|child| child.to_markdown())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n"),
|
||||
Node::Paragraph(paragraph) => paragraph.to_markdown(),
|
||||
Node::Heading { level, children } => {
|
||||
let hashes = "#".repeat(*level as usize);
|
||||
format!("{} {}", hashes, children.to_markdown())
|
||||
}
|
||||
Node::Blockquote(paragraph) => {
|
||||
let content = paragraph.to_markdown();
|
||||
content
|
||||
.lines()
|
||||
.map(|line| format!("> {}", line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
Node::List { children, ordered } => children
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, child)| {
|
||||
let prefix = if *ordered {
|
||||
format!("{}. ", i + 1)
|
||||
} else {
|
||||
"- ".to_string()
|
||||
};
|
||||
format!("{}{}", prefix, child.to_markdown())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
Node::ListItem {
|
||||
children, checked, ..
|
||||
} => {
|
||||
let checkbox = if let Some(checked) = checked {
|
||||
if *checked {
|
||||
"[x] "
|
||||
} else {
|
||||
"[ ] "
|
||||
}
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!(
|
||||
"{}{}",
|
||||
checkbox,
|
||||
children
|
||||
.iter()
|
||||
.map(|child| child.to_markdown())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
Node::CodeBlock { code, lang } => {
|
||||
format!("```{}\n{}\n```", lang.clone().unwrap_or_default(), code)
|
||||
}
|
||||
Node::Table(table) => {
|
||||
let header = table
|
||||
.children
|
||||
.first()
|
||||
.map(|row| {
|
||||
row.children
|
||||
.iter()
|
||||
.map(|cell| cell.children.to_markdown())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | ")
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let alignments = table
|
||||
.column_aligns
|
||||
.iter()
|
||||
.map(|align| {
|
||||
match align {
|
||||
TableColumnAlign::Left => ":--",
|
||||
TableColumnAlign::Center => ":-:",
|
||||
TableColumnAlign::Right => "--:",
|
||||
}
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | ");
|
||||
let rows = table
|
||||
.children
|
||||
.iter()
|
||||
.skip(1)
|
||||
.map(|row| {
|
||||
row.children
|
||||
.iter()
|
||||
.map(|cell| cell.children.to_markdown())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" | ")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
format!("{}\n{}\n{}", header, alignments, rows)
|
||||
}
|
||||
Node::Break { html } => {
|
||||
if *html {
|
||||
"<br>".to_string()
|
||||
} else {
|
||||
"\n".to_string()
|
||||
}
|
||||
}
|
||||
Node::Divider => "---".to_string(),
|
||||
Node::Ignore => "".to_string(),
|
||||
Node::Unknown => "".to_string(),
|
||||
}
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ pub(super) fn parse_html(source: &str) -> Result<element::Node, SharedString> {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
let bytes = minify_html::minify(source.as_bytes(), &minify_html::Cfg::default());
|
||||
let bytes = cleanup_html(&source);
|
||||
let mut cursor = std::io::Cursor::new(bytes);
|
||||
// Ref
|
||||
// https://github.com/servo/html5ever/blob/main/rcdom/examples/print-rcdom.rs
|
||||
|
|
@ -79,6 +79,12 @@ pub(super) fn parse_html(source: &str) -> Result<element::Node, SharedString> {
|
|||
Ok(node)
|
||||
}
|
||||
|
||||
fn cleanup_html(source: &str) -> Vec<u8> {
|
||||
let mut cfg = minify_html::Cfg::default();
|
||||
cfg.keep_closing_tags = true;
|
||||
minify_html::minify(source.as_bytes(), &cfg)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct HtmlElement {
|
||||
id: ElementId,
|
||||
|
|
@ -334,6 +340,7 @@ fn parse_table_cell(
|
|||
///
|
||||
/// - Before: " \r\n Hello world \t "
|
||||
/// - After: " Hello world "
|
||||
#[allow(dead_code)]
|
||||
fn trim_text(text: &str) -> String {
|
||||
let mut out = String::with_capacity(text.len());
|
||||
|
||||
|
|
@ -372,7 +379,7 @@ fn parse_paragraph(
|
|||
|
||||
match &node.data {
|
||||
NodeData::Text { ref contents } => {
|
||||
let part = trim_text(&contents.borrow());
|
||||
let part = &contents.borrow();
|
||||
text.push_str(&part);
|
||||
paragraph.push_str(&text);
|
||||
}
|
||||
|
|
@ -524,7 +531,7 @@ fn parse_paragraph(
|
|||
fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
|
||||
match node.data {
|
||||
NodeData::Text { ref contents } => {
|
||||
let text = contents.borrow().trim_start().to_string();
|
||||
let text = contents.borrow().to_string();
|
||||
if text.len() > 0 {
|
||||
paragraph.push_str(&text);
|
||||
}
|
||||
|
|
@ -536,7 +543,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
|
|||
ref attrs,
|
||||
..
|
||||
} => match name.local {
|
||||
local_name!("br") => element::Node::Break,
|
||||
local_name!("br") => element::Node::Break { html: true },
|
||||
local_name!("h1")
|
||||
| local_name!("h2")
|
||||
| local_name!("h3")
|
||||
|
|
@ -760,11 +767,74 @@ mod tests {
|
|||
|
||||
use super::trim_text;
|
||||
|
||||
#[test]
|
||||
fn test_cleanup_html() {
|
||||
let html = r#"<p>
|
||||
and
|
||||
<code>code</code>
|
||||
text
|
||||
</p>"#;
|
||||
let cleaned = super::cleanup_html(html);
|
||||
assert_eq!(
|
||||
String::from_utf8(cleaned).unwrap(),
|
||||
"<p>and <code>code</code> text</p>"
|
||||
);
|
||||
|
||||
let html = r#"<p>
|
||||
and
|
||||
<em> <code>code</code> <i>italic</i> </em>
|
||||
text
|
||||
</p>"#;
|
||||
let cleaned = super::cleanup_html(html);
|
||||
assert_eq!(
|
||||
String::from_utf8(cleaned).unwrap(),
|
||||
"<p>and <em> <code>code</code> <i>italic</i> </em> text</p>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trim_text() {
|
||||
assert_eq!(trim_text(" \n\tHello world \t\r "), " Hello world ",);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keep_spaces() {
|
||||
let html = r#"<p>and <code>code</code> text</p>"#;
|
||||
let node = super::parse_html(html).unwrap();
|
||||
assert_eq!(node.to_markdown(), "and `code` text");
|
||||
|
||||
let html = r#"
|
||||
<div>
|
||||
<p>
|
||||
and
|
||||
<em> <code>code</code> <i>italic</i> </em>
|
||||
text
|
||||
</p>
|
||||
<p>
|
||||
<img src="https://example.com/image.png" alt="Example" width="100" height="200" title="Example Image" />
|
||||
</p>
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
"#;
|
||||
let node = super::parse_html(html).unwrap();
|
||||
assert_eq!(
|
||||
node.to_markdown(),
|
||||
indoc::indoc! {r#"
|
||||
and * code italic * text
|
||||
|
||||

|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
"#}
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_value_to_length() {
|
||||
assert_eq!(super::value_to_length("100px"), Some(px(100.).into()));
|
||||
|
|
|
|||
|
|
@ -20,17 +20,7 @@ use super::{
|
|||
/// This is design goal is to be able to most common Markdown (GFM) features
|
||||
/// to let us to display rich text in our application.
|
||||
///
|
||||
/// The goal:
|
||||
///
|
||||
/// - For used to help message.
|
||||
/// - For used to display like about page.
|
||||
/// - Some general style customization (Like base text size, line-height...).
|
||||
///
|
||||
/// Not in goal:
|
||||
///
|
||||
/// - As a markdown editor.
|
||||
/// - Add custom markdown syntax.
|
||||
/// - Complex styles cumstomization.
|
||||
/// See also [`super::TextView`]
|
||||
#[derive(Clone)]
|
||||
pub(super) struct MarkdownElement {
|
||||
id: ElementId,
|
||||
|
|
@ -75,11 +65,7 @@ impl MarkdownState {
|
|||
}
|
||||
|
||||
self.raw = new_text;
|
||||
self.root = Some(
|
||||
markdown::to_mdast(&self.raw, &ParseOptions::gfm())
|
||||
.map(|n| n.into())
|
||||
.map_err(|e| e.to_string().into()),
|
||||
);
|
||||
self.root = Some(parse_markdown(&self.raw));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +142,13 @@ impl Element for MarkdownElement {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parse Markdown into a tree of nodes.
|
||||
fn parse_markdown(raw: &str) -> Result<element::Node, SharedString> {
|
||||
markdown::to_mdast(&raw, &ParseOptions::gfm())
|
||||
.map(|n| n.into())
|
||||
.map_err(|e| e.to_string().into())
|
||||
}
|
||||
|
||||
fn parse_table_row(table: &mut Table, node: &mdast::TableRow) {
|
||||
let mut row = TableRow::default();
|
||||
node.children.iter().for_each(|c| {
|
||||
|
|
@ -312,8 +305,12 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
}
|
||||
Node::Html(val) => match parse_html(&val.value) {
|
||||
Ok(el) => {
|
||||
if el == element::Node::Break {
|
||||
text.push_str("\n");
|
||||
if el.is_break() {
|
||||
text = "\n".to_owned();
|
||||
paragraph.push(element::TextNode {
|
||||
text: text.clone(),
|
||||
marks: vec![(0..text.len(), InlineTextStyle::default())],
|
||||
});
|
||||
} else {
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!("[markdown] unsupported inline html tag: {:#?}", el);
|
||||
|
|
@ -376,7 +373,7 @@ impl From<mdast::Node> for element::Node {
|
|||
checked: val.checked,
|
||||
}
|
||||
}
|
||||
Node::Break(_) => element::Node::Break,
|
||||
Node::Break(_) => element::Node::Break { html: false },
|
||||
Node::Code(raw) => element::Node::CodeBlock {
|
||||
code: raw.value.into(),
|
||||
lang: raw.lang.map(|s| s.into()),
|
||||
|
|
@ -460,3 +457,18 @@ impl From<mdast::Node> for element::Node {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_markdown;
|
||||
|
||||
#[test]
|
||||
fn test_parse_br() {
|
||||
let raw = "Row 1<br/>Row 2<br>[Link](https://github.com)";
|
||||
let node = parse_markdown(&raw).unwrap();
|
||||
assert_eq!(
|
||||
node.to_markdown(),
|
||||
"Row 1\nRow 2\n[Link](https://github.com)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,21 @@ use gpui::{rems, App, ElementId, IntoElement, Rems, RenderOnce, SharedString, Wi
|
|||
use super::{html::HtmlElement, markdown::MarkdownElement};
|
||||
|
||||
/// A text view that can render Markdown or HTML.
|
||||
///
|
||||
/// ## Goals
|
||||
///
|
||||
/// - Provide a rich text rendering component for such as Markdown or HTML,
|
||||
/// used to display rich text in GPUI application (e.g., Help messages, Release notes)
|
||||
/// - Support Markdown GFM and HTML (Simple HTML like Safari Reader Mode) for showing most common used markups.
|
||||
/// - Support Heading, Paragraph, Bold, Italic, StrikeThrough, Code, Link, Image, Blockquote, List, Table, HorizontalRule, CodeBlock ...
|
||||
///
|
||||
/// ## Not Goals
|
||||
///
|
||||
/// - Customization of the complex style (some simple styles will be supported)
|
||||
/// - As a Markdown editor or viewer (If you want to like this, you must fork your version).
|
||||
/// - As a HTML viewer, we not support CSS, we only support basic HTML tags for used to as a content reader.
|
||||
///
|
||||
/// See also [`MarkdownElement`], [`HtmlElement`]
|
||||
#[allow(private_interfaces)]
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub enum TextView {
|
||||
|
|
|
|||
Loading…
Reference in a new issue