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

|

|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
这是一个中文演示段落,用于展示更多的 [Markdown GFM](https://github.github.com/gfm/) 内容。您可以在此尝试使用使用**粗体**、*斜体*和`代码`等样式。これは日本語のデモ段落です。Markdown の多言語サポートを示すためのテキストが含まれています。例えば、、**ボールド**、_イタリック_、および`コード`のスタイルなどを試すことができます。
|
这是一个中文演示段落,用于展示更多的 [Markdown GFM](https://github.github.com/gfm/) 内容。您可以在此尝试使用使用**粗体**、*斜体*和`代码`等样式。これは日本語のデモ段落です。Markdown の多言語サポートを示すためのテキストが含まれています。例えば、、**ボールド**、_イタリック_、および`コード`のスタイルなどを試すことができます。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Heading for [Links](https://www.google.com)
|
## 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).
|
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
|
### HTML
|
||||||
|
|
||||||
#### Paragraph and Text
|
#### 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" width="100%" />
|
||||||
<img src="https://www.rust-lang.org/logos/rust-logo-blk.svg" alt="Rust" style="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
|
## Unsupported
|
||||||
|
|
||||||
### HTML
|
### HTML
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ once_cell = "1.19.0"
|
||||||
paste = "1"
|
paste = "1"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
resvg = { version = "0.44.0", default-features = false, features = [
|
resvg = { version = "0.44.0", default-features = false, features = [
|
||||||
"system-fonts",
|
"system-fonts",
|
||||||
"text",
|
"text",
|
||||||
] }
|
] }
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
rust-i18n = "3"
|
rust-i18n = "3"
|
||||||
|
|
@ -34,20 +34,22 @@ smallvec = "1.13.2"
|
||||||
smol = "1"
|
smol = "1"
|
||||||
unicode-segmentation = "1.12.0"
|
unicode-segmentation = "1.12.0"
|
||||||
usvg = { version = "0.44.0", default-features = false, features = [
|
usvg = { version = "0.44.0", default-features = false, features = [
|
||||||
"system-fonts",
|
"system-fonts",
|
||||||
"text",
|
"text",
|
||||||
] }
|
] }
|
||||||
uuid = "1.10"
|
uuid = "1.10"
|
||||||
wry = { version = "0.48.0", optional = true }
|
wry = { version = "0.48.0", optional = true }
|
||||||
# Markdown Parser
|
# Markdown Parser
|
||||||
markdown = "1.0.0-alpha.22"
|
markdown = "1.0.0-alpha.22"
|
||||||
# HTML Parser
|
# HTML Parser
|
||||||
html5ever = "0.27"
|
html5ever = "0.27"
|
||||||
markup5ever_rcdom = "0.3.0"
|
markup5ever_rcdom = "0.3.0"
|
||||||
minify-html = "0.15.0"
|
minify-html = "0.15.0"
|
||||||
|
|
||||||
# Calendar
|
# Calendar
|
||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
indoc = "2"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -229,22 +229,27 @@ pub enum Node {
|
||||||
lang: Option<SharedString>,
|
lang: Option<SharedString>,
|
||||||
},
|
},
|
||||||
Table(Table),
|
Table(Table),
|
||||||
// <br>
|
Break {
|
||||||
Break,
|
html: bool,
|
||||||
|
},
|
||||||
Divider,
|
Divider,
|
||||||
Ignore,
|
Ignore,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
impl Node {
|
||||||
fn is_ignore(&self) -> bool {
|
pub(super) fn is_ignore(&self) -> bool {
|
||||||
matches!(self, Self::Ignore)
|
matches!(self, Self::Ignore)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_list_item(&self) -> bool {
|
pub(super) fn is_list_item(&self) -> bool {
|
||||||
matches!(self, Self::ListItem { .. })
|
matches!(self, Self::ListItem { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn is_break(&self) -> bool {
|
||||||
|
matches!(self, Self::Break { .. })
|
||||||
|
}
|
||||||
|
|
||||||
/// Combine all children, omitting the empt parent nodes.
|
/// Combine all children, omitting the empt parent nodes.
|
||||||
pub(super) fn compact(&self) -> Node {
|
pub(super) fn compact(&self) -> Node {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -276,7 +281,14 @@ impl RenderOnce for Paragraph {
|
||||||
|
|
||||||
for text_node in children.into_iter() {
|
for text_node in children.into_iter() {
|
||||||
let text_len = text_node.text.len();
|
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![];
|
let mut node_highlights = vec![];
|
||||||
for (range, style) in text_node.marks {
|
for (range, style) in text_node.marks {
|
||||||
|
|
@ -581,7 +593,6 @@ impl Node {
|
||||||
.border_l_3()
|
.border_l_3()
|
||||||
.border_color(cx.theme().secondary_active)
|
.border_color(cx.theme().secondary_active)
|
||||||
.px_4()
|
.px_4()
|
||||||
.py_1()
|
|
||||||
.child(children)
|
.child(children)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
Node::List { children, ordered } => v_flex()
|
Node::List { children, ordered } => v_flex()
|
||||||
|
|
@ -628,7 +639,7 @@ impl Node {
|
||||||
.h(px(2.))
|
.h(px(2.))
|
||||||
.mb(mb)
|
.mb(mb)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
Node::Break => div().into_any_element(),
|
Node::Break { .. } => div().into_any_element(),
|
||||||
Node::Ignore => div().into_any_element(),
|
Node::Ignore => div().into_any_element(),
|
||||||
_ => {
|
_ => {
|
||||||
if cfg!(debug_assertions) {
|
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()
|
..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);
|
let mut cursor = std::io::Cursor::new(bytes);
|
||||||
// Ref
|
// Ref
|
||||||
// https://github.com/servo/html5ever/blob/main/rcdom/examples/print-rcdom.rs
|
// 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)
|
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)]
|
#[derive(Clone)]
|
||||||
pub(super) struct HtmlElement {
|
pub(super) struct HtmlElement {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
|
@ -334,6 +340,7 @@ fn parse_table_cell(
|
||||||
///
|
///
|
||||||
/// - Before: " \r\n Hello world \t "
|
/// - Before: " \r\n Hello world \t "
|
||||||
/// - After: " Hello world "
|
/// - After: " Hello world "
|
||||||
|
#[allow(dead_code)]
|
||||||
fn trim_text(text: &str) -> String {
|
fn trim_text(text: &str) -> String {
|
||||||
let mut out = String::with_capacity(text.len());
|
let mut out = String::with_capacity(text.len());
|
||||||
|
|
||||||
|
|
@ -372,7 +379,7 @@ fn parse_paragraph(
|
||||||
|
|
||||||
match &node.data {
|
match &node.data {
|
||||||
NodeData::Text { ref contents } => {
|
NodeData::Text { ref contents } => {
|
||||||
let part = trim_text(&contents.borrow());
|
let part = &contents.borrow();
|
||||||
text.push_str(&part);
|
text.push_str(&part);
|
||||||
paragraph.push_str(&text);
|
paragraph.push_str(&text);
|
||||||
}
|
}
|
||||||
|
|
@ -524,7 +531,7 @@ fn parse_paragraph(
|
||||||
fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
|
fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
|
||||||
match node.data {
|
match node.data {
|
||||||
NodeData::Text { ref contents } => {
|
NodeData::Text { ref contents } => {
|
||||||
let text = contents.borrow().trim_start().to_string();
|
let text = contents.borrow().to_string();
|
||||||
if text.len() > 0 {
|
if text.len() > 0 {
|
||||||
paragraph.push_str(&text);
|
paragraph.push_str(&text);
|
||||||
}
|
}
|
||||||
|
|
@ -536,7 +543,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> element::Node {
|
||||||
ref attrs,
|
ref attrs,
|
||||||
..
|
..
|
||||||
} => match name.local {
|
} => match name.local {
|
||||||
local_name!("br") => element::Node::Break,
|
local_name!("br") => element::Node::Break { html: true },
|
||||||
local_name!("h1")
|
local_name!("h1")
|
||||||
| local_name!("h2")
|
| local_name!("h2")
|
||||||
| local_name!("h3")
|
| local_name!("h3")
|
||||||
|
|
@ -760,11 +767,74 @@ mod tests {
|
||||||
|
|
||||||
use super::trim_text;
|
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]
|
#[test]
|
||||||
fn test_trim_text() {
|
fn test_trim_text() {
|
||||||
assert_eq!(trim_text(" \n\tHello world \t\r "), " Hello world ",);
|
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]
|
#[test]
|
||||||
fn test_value_to_length() {
|
fn test_value_to_length() {
|
||||||
assert_eq!(super::value_to_length("100px"), Some(px(100.).into()));
|
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
|
/// This is design goal is to be able to most common Markdown (GFM) features
|
||||||
/// to let us to display rich text in our application.
|
/// to let us to display rich text in our application.
|
||||||
///
|
///
|
||||||
/// The goal:
|
/// See also [`super::TextView`]
|
||||||
///
|
|
||||||
/// - 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.
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(super) struct MarkdownElement {
|
pub(super) struct MarkdownElement {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
|
@ -75,11 +65,7 @@ impl MarkdownState {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.raw = new_text;
|
self.raw = new_text;
|
||||||
self.root = Some(
|
self.root = Some(parse_markdown(&self.raw));
|
||||||
markdown::to_mdast(&self.raw, &ParseOptions::gfm())
|
|
||||||
.map(|n| n.into())
|
|
||||||
.map_err(|e| e.to_string().into()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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) {
|
fn parse_table_row(table: &mut Table, node: &mdast::TableRow) {
|
||||||
let mut row = TableRow::default();
|
let mut row = TableRow::default();
|
||||||
node.children.iter().for_each(|c| {
|
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) {
|
Node::Html(val) => match parse_html(&val.value) {
|
||||||
Ok(el) => {
|
Ok(el) => {
|
||||||
if el == element::Node::Break {
|
if el.is_break() {
|
||||||
text.push_str("\n");
|
text = "\n".to_owned();
|
||||||
|
paragraph.push(element::TextNode {
|
||||||
|
text: text.clone(),
|
||||||
|
marks: vec![(0..text.len(), InlineTextStyle::default())],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
eprintln!("[markdown] unsupported inline html tag: {:#?}", el);
|
eprintln!("[markdown] unsupported inline html tag: {:#?}", el);
|
||||||
|
|
@ -376,7 +373,7 @@ impl From<mdast::Node> for element::Node {
|
||||||
checked: val.checked,
|
checked: val.checked,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Node::Break(_) => element::Node::Break,
|
Node::Break(_) => element::Node::Break { html: false },
|
||||||
Node::Code(raw) => element::Node::CodeBlock {
|
Node::Code(raw) => element::Node::CodeBlock {
|
||||||
code: raw.value.into(),
|
code: raw.value.into(),
|
||||||
lang: raw.lang.map(|s| s.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};
|
use super::{html::HtmlElement, markdown::MarkdownElement};
|
||||||
|
|
||||||
/// A text view that can render Markdown or HTML.
|
/// 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)]
|
#[allow(private_interfaces)]
|
||||||
#[derive(IntoElement, Clone)]
|
#[derive(IntoElement, Clone)]
|
||||||
pub enum TextView {
|
pub enum TextView {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue