markdown: Add to support LinkReference and Footnote. (#1201)
This commit is contained in:
parent
f4a292d975
commit
ee40f65fb5
6 changed files with 232 additions and 81 deletions
|
|
@ -2,13 +2,19 @@
|
|||
|
||||
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 [^1] [^2].
|
||||
|
||||
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]. It includes various stylistic elements and plain text.
|
||||
|
||||

|
||||
|
||||
这是一个中文演示段落,用于展示更多的 [Markdown GFM](https://github.github.com/gfm/) 内容。您可以在此尝试使用使用**粗体**、*斜体*和`代码`等样式。これは日本語のデモ段落です。Markdown の多言語サポートを示すためのテキストが含まれています。例えば、、**ボールド**、_イタリック_、および`コード`のスタイルなどを試すことができます。
|
||||
这是一个中文演示段落,用于展示更多的 [Markdown GFM] 内容。您可以在此尝试使用使用**粗体**、*斜体*和`代码`等样式。これは日本語のデモ段落です。Markdown の多言語サポートを示すためのテキストが含まれています。例えば、、**ボールド**、_イタリック_、および`コード`のスタイルなどを試すことができます。
|
||||
|
||||
[Markdown GFM]: https://github.github.com/gfm/
|
||||
|
||||
[^1]: This is a footnote example.
|
||||
|
||||
[^2]: Here is another footnote.
|
||||
|
||||
## Basic formatting
|
||||
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ impl SyntaxHighlighter {
|
|||
selected_range: &Range<usize>,
|
||||
full_text: &SharedString,
|
||||
new_text: &str,
|
||||
cx: &mut App,
|
||||
cx: &App,
|
||||
) {
|
||||
if &self.text == full_text {
|
||||
return;
|
||||
|
|
@ -324,7 +324,7 @@ impl SyntaxHighlighter {
|
|||
/// NOTE: 10K lines, about 180ms
|
||||
/// FIXME: To improve the performance when there more than 5K lines, use partial update.
|
||||
/// Ref: https://github.com/longbridge/gpui-component/pull/1197
|
||||
fn build_styles(&mut self, cx: &mut App) {
|
||||
fn build_styles(&mut self, cx: &App) {
|
||||
let Some(tree) = &self.old_tree else {
|
||||
return;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use crate::text::TextViewState;
|
|||
use crate::v_flex;
|
||||
|
||||
use crate::text::node::{
|
||||
self, ImageNode, InlineNode, LinkMark, Paragraph, Table, TableRow, TextMark,
|
||||
self, ImageNode, InlineNode, LinkMark, NodeContext, Paragraph, Table, TableRow, TextMark,
|
||||
};
|
||||
use crate::text::TextViewStyle;
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ const BLOCK_ELEMENTS: [&str; 35] = [
|
|||
];
|
||||
|
||||
/// Parse HTML into AST Node.
|
||||
pub(crate) fn parse(source: &str) -> Result<node::Node, SharedString> {
|
||||
pub(crate) fn parse(source: &str, cx: &mut NodeContext) -> Result<node::Node, SharedString> {
|
||||
let opts = ParseOpts {
|
||||
..Default::default()
|
||||
};
|
||||
|
|
@ -77,7 +77,8 @@ pub(crate) fn parse(source: &str) -> Result<node::Node, SharedString> {
|
|||
|
||||
let mut paragraph = Paragraph::default();
|
||||
// NOTE: The outer paragraph is not used.
|
||||
let node: node::Node = parse_node(&dom.document, &mut paragraph).unwrap_or(node::Node::Unknown);
|
||||
let node: node::Node =
|
||||
parse_node(&dom.document, &mut paragraph, cx).unwrap_or(node::Node::Unknown);
|
||||
let node = node.compact();
|
||||
|
||||
Ok(node)
|
||||
|
|
@ -131,8 +132,10 @@ impl RenderOnce for HtmlElement {
|
|||
});
|
||||
|
||||
let root = self.state.read(cx).root();
|
||||
let node_cx = self.state.read(cx).node_cx.clone();
|
||||
|
||||
div().map(|this| match root {
|
||||
Ok(node) => this.child(node.render(None, true, true, &self.style, window, cx)),
|
||||
Ok(node) => this.child(node.render(None, true, true, &node_cx, window, cx)),
|
||||
Err(err) => this.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
|
|
@ -363,6 +366,7 @@ fn parse_paragraph(
|
|||
.unwrap_or_default()
|
||||
.into(),
|
||||
title: attr_value(&attrs, local_name!("title")).map(Into::into),
|
||||
..Default::default()
|
||||
}),
|
||||
));
|
||||
paragraph.push(InlineNode::new(&text).marks(marks.clone()));
|
||||
|
|
@ -411,7 +415,11 @@ fn parse_paragraph(
|
|||
(text, marks)
|
||||
}
|
||||
|
||||
fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<node::Node> {
|
||||
fn parse_node(
|
||||
node: &Rc<Node>,
|
||||
paragraph: &mut Paragraph,
|
||||
cx: &mut NodeContext,
|
||||
) -> Option<node::Node> {
|
||||
match node.data {
|
||||
NodeData::Text { ref contents } => {
|
||||
let text = contents.borrow().to_string();
|
||||
|
|
@ -495,7 +503,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<node::Node>
|
|||
}
|
||||
local_name!("ul") | local_name!("ol") => {
|
||||
let ordered = name.local == local_name!("ol");
|
||||
let children = consume_children_nodes(node, paragraph);
|
||||
let children = consume_children_nodes(node, paragraph, cx);
|
||||
Some(node::Node::List { children, ordered })
|
||||
}
|
||||
local_name!("li") => {
|
||||
|
|
@ -504,7 +512,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<node::Node>
|
|||
|
||||
for child in node.children.borrow().iter() {
|
||||
let mut child_paragraph = Paragraph::default();
|
||||
if let Some(child_node) = parse_node(child, &mut child_paragraph) {
|
||||
if let Some(child_node) = parse_node(child, &mut child_paragraph, cx) {
|
||||
children.push(child_node);
|
||||
}
|
||||
if child_paragraph.text_len() > 0 {
|
||||
|
|
@ -559,7 +567,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<node::Node>
|
|||
}
|
||||
}
|
||||
local_name!("blockquote") => {
|
||||
let children = consume_children_nodes(node, paragraph);
|
||||
let children = consume_children_nodes(node, paragraph, cx);
|
||||
Some(node::Node::Blockquote { children })
|
||||
}
|
||||
local_name!("style") | local_name!("script") => None,
|
||||
|
|
@ -576,7 +584,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<node::Node>
|
|||
|
||||
// Inner of the block element -- The "Inner text of block element"
|
||||
for child in node.children.borrow().iter() {
|
||||
if let Some(child_node) = parse_node(child, paragraph) {
|
||||
if let Some(child_node) = parse_node(child, paragraph, cx) {
|
||||
children.push(child_node);
|
||||
}
|
||||
}
|
||||
|
|
@ -602,7 +610,7 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<node::Node>
|
|||
}
|
||||
},
|
||||
NodeData::Document => {
|
||||
let children = consume_children_nodes(node, paragraph);
|
||||
let children = consume_children_nodes(node, paragraph, cx);
|
||||
Some(node::Node::Root { children })
|
||||
}
|
||||
NodeData::Doctype { .. }
|
||||
|
|
@ -611,11 +619,15 @@ fn parse_node(node: &Rc<Node>, paragraph: &mut Paragraph) -> Option<node::Node>
|
|||
}
|
||||
}
|
||||
|
||||
fn consume_children_nodes(node: &Node, paragraph: &mut Paragraph) -> Vec<node::Node> {
|
||||
fn consume_children_nodes(
|
||||
node: &Node,
|
||||
paragraph: &mut Paragraph,
|
||||
cx: &mut NodeContext,
|
||||
) -> Vec<node::Node> {
|
||||
let mut children = vec![];
|
||||
consume_paragraph(&mut children, paragraph);
|
||||
for child in node.children.borrow().iter() {
|
||||
if let Some(child_node) = parse_node(child, paragraph) {
|
||||
if let Some(child_node) = parse_node(child, paragraph, cx) {
|
||||
children.push(child_node);
|
||||
}
|
||||
consume_paragraph(&mut children, paragraph);
|
||||
|
|
@ -637,7 +649,7 @@ fn consume_paragraph(children: &mut Vec<node::Node>, paragraph: &mut Paragraph)
|
|||
mod tests {
|
||||
use gpui::{px, relative};
|
||||
|
||||
use crate::text::node::{ImageNode, InlineNode, Node, Paragraph};
|
||||
use crate::text::node::{ImageNode, InlineNode, Node, NodeContext, Paragraph};
|
||||
|
||||
use super::trim_text;
|
||||
|
||||
|
|
@ -674,7 +686,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_keep_spaces() {
|
||||
let html = r#"<p>and <code>code</code> text</p>"#;
|
||||
let node = super::parse(html).unwrap();
|
||||
let mut cx = NodeContext::default();
|
||||
let node = super::parse(html, &mut cx).unwrap();
|
||||
assert_eq!(node.to_markdown(), "and `code` text");
|
||||
|
||||
let html = r#"
|
||||
|
|
@ -694,7 +707,7 @@ mod tests {
|
|||
</ul>
|
||||
</div>
|
||||
"#;
|
||||
let node = super::parse(html).unwrap();
|
||||
let node = super::parse(html, &mut cx).unwrap();
|
||||
assert_eq!(
|
||||
node.to_markdown(),
|
||||
indoc::indoc! {r#"
|
||||
|
|
@ -720,7 +733,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_image() {
|
||||
let html = r#"<img src="https://example.com/image.png" alt="Example" width="100" height="200" title="Example Image" />"#;
|
||||
let node = super::parse(html).unwrap();
|
||||
let mut cx = NodeContext::default();
|
||||
let node = super::parse(html, &mut cx).unwrap();
|
||||
assert_eq!(
|
||||
node,
|
||||
Node::Paragraph(Paragraph {
|
||||
|
|
@ -738,7 +752,7 @@ mod tests {
|
|||
);
|
||||
|
||||
let html = r#"<img src="https://example.com/image.png" alt="Example" style="width: 80%" title="Example Image" />"#;
|
||||
let node = super::parse(html).unwrap();
|
||||
let node = super::parse(html, &mut cx).unwrap();
|
||||
assert_eq!(
|
||||
node,
|
||||
Node::Paragraph(Paragraph {
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ use markdown::{
|
|||
use crate::{
|
||||
text::{
|
||||
node::{
|
||||
self, CodeBlock, ImageNode, InlineNode, LinkMark, Paragraph, Span, Table, TableRow,
|
||||
TextMark,
|
||||
self, CodeBlock, ImageNode, InlineNode, LinkMark, NodeContext, Paragraph, Span, Table,
|
||||
TableRow, TextMark,
|
||||
},
|
||||
TextViewState, TextViewStyle,
|
||||
},
|
||||
|
|
@ -60,8 +60,10 @@ impl RenderOnce for MarkdownElement {
|
|||
});
|
||||
|
||||
let root = self.state.read(cx).root();
|
||||
let node_cx = self.state.read(cx).node_cx.clone();
|
||||
|
||||
div().map(|this| match root {
|
||||
Ok(node) => this.child(node.render(None, true, true, &self.style, window, cx)),
|
||||
Ok(node) => this.child(node.render(None, true, true, &node_cx, window, cx)),
|
||||
Err(err) => this.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
|
|
@ -76,19 +78,20 @@ impl RenderOnce for MarkdownElement {
|
|||
pub(crate) fn parse(
|
||||
raw: &str,
|
||||
style: &TextViewStyle,
|
||||
cx: &mut App,
|
||||
cx: &mut NodeContext,
|
||||
app: &App,
|
||||
) -> Result<node::Node, SharedString> {
|
||||
markdown::to_mdast(&raw, &ParseOptions::gfm())
|
||||
.map(|n| ast_to_node(n, style, cx))
|
||||
.map(|n| ast_to_node(n, style, cx, app))
|
||||
.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, cx: &mut NodeContext) {
|
||||
let mut row = TableRow::default();
|
||||
node.children.iter().for_each(|c| {
|
||||
match c {
|
||||
Node::TableCell(cell) => {
|
||||
parse_table_cell(&mut row, cell);
|
||||
parse_table_cell(&mut row, cell, cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
|
@ -96,10 +99,10 @@ fn parse_table_row(table: &mut Table, node: &mdast::TableRow) {
|
|||
table.children.push(row);
|
||||
}
|
||||
|
||||
fn parse_table_cell(row: &mut node::TableRow, node: &mdast::TableCell) {
|
||||
fn parse_table_cell(row: &mut node::TableRow, node: &mdast::TableCell, cx: &mut NodeContext) {
|
||||
let mut paragraph = Paragraph::default();
|
||||
node.children.iter().for_each(|c| {
|
||||
parse_paragraph(&mut paragraph, c);
|
||||
parse_paragraph(&mut paragraph, c, cx);
|
||||
});
|
||||
let table_cell = node::TableCell {
|
||||
children: paragraph,
|
||||
|
|
@ -108,7 +111,7 @@ fn parse_table_cell(row: &mut node::TableRow, node: &mdast::TableCell) {
|
|||
row.children.push(table_cell);
|
||||
}
|
||||
|
||||
fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
||||
fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node, cx: &mut NodeContext) -> String {
|
||||
let span = node.position().map(|pos| Span {
|
||||
start: pos.start.offset,
|
||||
end: pos.end.offset,
|
||||
|
|
@ -122,7 +125,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
match node {
|
||||
Node::Paragraph(val) => {
|
||||
val.children.iter().for_each(|c| {
|
||||
text.push_str(&parse_paragraph(paragraph, c));
|
||||
text.push_str(&parse_paragraph(paragraph, c, cx));
|
||||
});
|
||||
}
|
||||
Node::Text(val) => {
|
||||
|
|
@ -132,7 +135,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
Node::Emphasis(val) => {
|
||||
let mut child_paragraph = Paragraph::default();
|
||||
for child in val.children.iter() {
|
||||
text.push_str(&parse_paragraph(&mut child_paragraph, &child));
|
||||
text.push_str(&parse_paragraph(&mut child_paragraph, &child, cx));
|
||||
}
|
||||
paragraph.push(
|
||||
InlineNode::new(&text).marks(vec![(0..text.len(), TextMark::default().italic())]),
|
||||
|
|
@ -141,7 +144,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
Node::Strong(val) => {
|
||||
let mut child_paragraph = Paragraph::default();
|
||||
for child in val.children.iter() {
|
||||
text.push_str(&parse_paragraph(&mut child_paragraph, &child));
|
||||
text.push_str(&parse_paragraph(&mut child_paragraph, &child, cx));
|
||||
}
|
||||
paragraph.push(
|
||||
InlineNode::new(&text).marks(vec![(0..text.len(), TextMark::default().bold())]),
|
||||
|
|
@ -150,7 +153,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
Node::Delete(val) => {
|
||||
let mut child_paragraph = Paragraph::default();
|
||||
for child in val.children.iter() {
|
||||
text.push_str(&parse_paragraph(&mut child_paragraph, &child));
|
||||
text.push_str(&parse_paragraph(&mut child_paragraph, &child, cx));
|
||||
}
|
||||
paragraph.push(
|
||||
InlineNode::new(&text)
|
||||
|
|
@ -167,11 +170,12 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
let link_mark = Some(LinkMark {
|
||||
url: val.url.clone().into(),
|
||||
title: val.title.clone().map(|s| s.into()),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut child_paragraph = Paragraph::default();
|
||||
for child in val.children.iter() {
|
||||
text.push_str(&parse_paragraph(&mut child_paragraph, &child));
|
||||
text.push_str(&parse_paragraph(&mut child_paragraph, &child, cx));
|
||||
}
|
||||
|
||||
// FIXME: GPUI InteractiveText does not support inline images yet.
|
||||
|
|
@ -211,7 +215,7 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
paragraph
|
||||
.push(InlineNode::new(&text).marks(vec![(0..text.len(), TextMark::default())]));
|
||||
}
|
||||
Node::Html(val) => match super::html::parse(&val.value) {
|
||||
Node::Html(val) => match super::html::parse(&val.value, cx) {
|
||||
Ok(el) => {
|
||||
if el.is_break() {
|
||||
text = "\n".to_owned();
|
||||
|
|
@ -230,6 +234,37 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
text.push_str(&val.value);
|
||||
}
|
||||
},
|
||||
Node::FootnoteReference(foot) => {
|
||||
let prefix = format!("[{}]", foot.identifier);
|
||||
paragraph.push(InlineNode::new(&prefix).marks(vec![(
|
||||
0..prefix.len(),
|
||||
TextMark {
|
||||
italic: true,
|
||||
..Default::default()
|
||||
},
|
||||
)]));
|
||||
}
|
||||
Node::LinkReference(link) => {
|
||||
let mut child_paragraph = Paragraph::default();
|
||||
let mut child_text = String::new();
|
||||
for child in link.children.iter() {
|
||||
child_text.push_str(&parse_paragraph(&mut child_paragraph, child, cx));
|
||||
}
|
||||
|
||||
let link_mark = LinkMark {
|
||||
url: "".into(),
|
||||
title: link.label.clone().map(Into::into),
|
||||
identifier: Some(link.identifier.clone().into()),
|
||||
};
|
||||
|
||||
paragraph.push(InlineNode::new(&child_text).marks(vec![(
|
||||
0..child_text.len(),
|
||||
TextMark {
|
||||
link: Some(link_mark),
|
||||
..Default::default()
|
||||
},
|
||||
)]));
|
||||
}
|
||||
_ => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::warn!("unsupported inline node: {:#?}", node);
|
||||
|
|
@ -240,20 +275,25 @@ fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String {
|
|||
text
|
||||
}
|
||||
|
||||
fn ast_to_node(value: mdast::Node, style: &TextViewStyle, cx: &mut App) -> node::Node {
|
||||
fn ast_to_node(
|
||||
value: mdast::Node,
|
||||
style: &TextViewStyle,
|
||||
cx: &mut NodeContext,
|
||||
app: &App,
|
||||
) -> node::Node {
|
||||
match value {
|
||||
Node::Root(val) => {
|
||||
let children = val
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|c| ast_to_node(c, style, cx))
|
||||
.map(|c| ast_to_node(c, style, cx, app))
|
||||
.collect();
|
||||
node::Node::Root { children }
|
||||
}
|
||||
Node::Paragraph(val) => {
|
||||
let mut paragraph = Paragraph::default();
|
||||
val.children.iter().for_each(|c| {
|
||||
parse_paragraph(&mut paragraph, c);
|
||||
parse_paragraph(&mut paragraph, c, cx);
|
||||
});
|
||||
|
||||
node::Node::Paragraph(paragraph)
|
||||
|
|
@ -262,7 +302,7 @@ fn ast_to_node(value: mdast::Node, style: &TextViewStyle, cx: &mut App) -> node:
|
|||
let children = val
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|c| ast_to_node(c, style, cx))
|
||||
.map(|c| ast_to_node(c, style, cx, app))
|
||||
.collect();
|
||||
node::Node::Blockquote { children }
|
||||
}
|
||||
|
|
@ -270,7 +310,7 @@ fn ast_to_node(value: mdast::Node, style: &TextViewStyle, cx: &mut App) -> node:
|
|||
let children = list
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|c| ast_to_node(c, style, cx))
|
||||
.map(|c| ast_to_node(c, style, cx, app))
|
||||
.collect();
|
||||
node::Node::List {
|
||||
ordered: list.ordered,
|
||||
|
|
@ -281,7 +321,7 @@ fn ast_to_node(value: mdast::Node, style: &TextViewStyle, cx: &mut App) -> node:
|
|||
let children = val
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|c| ast_to_node(c, style, cx))
|
||||
.map(|c| ast_to_node(c, style, cx, app))
|
||||
.collect();
|
||||
node::Node::ListItem {
|
||||
children,
|
||||
|
|
@ -294,12 +334,12 @@ fn ast_to_node(value: mdast::Node, style: &TextViewStyle, cx: &mut App) -> node:
|
|||
raw.value.into(),
|
||||
raw.lang.map(|s| s.into()),
|
||||
style,
|
||||
cx,
|
||||
app,
|
||||
)),
|
||||
Node::Heading(val) => {
|
||||
let mut paragraph = Paragraph::default();
|
||||
val.children.iter().for_each(|c| {
|
||||
parse_paragraph(&mut paragraph, c);
|
||||
parse_paragraph(&mut paragraph, c, cx);
|
||||
});
|
||||
|
||||
node::Node::Heading {
|
||||
|
|
@ -307,8 +347,10 @@ fn ast_to_node(value: mdast::Node, style: &TextViewStyle, cx: &mut App) -> node:
|
|||
children: paragraph,
|
||||
}
|
||||
}
|
||||
Node::Math(val) => node::Node::CodeBlock(CodeBlock::new(val.value.into(), None, style, cx)),
|
||||
Node::Html(val) => match super::html::parse(&val.value) {
|
||||
Node::Math(val) => {
|
||||
node::Node::CodeBlock(CodeBlock::new(val.value.into(), None, style, app))
|
||||
}
|
||||
Node::Html(val) => match super::html::parse(&val.value, cx) {
|
||||
Ok(el) => el,
|
||||
Err(err) => {
|
||||
if cfg!(debug_assertions) {
|
||||
|
|
@ -322,31 +364,31 @@ fn ast_to_node(value: mdast::Node, style: &TextViewStyle, cx: &mut App) -> node:
|
|||
val.value.into(),
|
||||
Some("mdx".into()),
|
||||
style,
|
||||
cx,
|
||||
app,
|
||||
)),
|
||||
Node::Yaml(val) => node::Node::CodeBlock(CodeBlock::new(
|
||||
val.value.into(),
|
||||
Some("yml".into()),
|
||||
style,
|
||||
cx,
|
||||
app,
|
||||
)),
|
||||
Node::Toml(val) => node::Node::CodeBlock(CodeBlock::new(
|
||||
val.value.into(),
|
||||
Some("toml".into()),
|
||||
style,
|
||||
cx,
|
||||
app,
|
||||
)),
|
||||
Node::MdxJsxTextElement(val) => {
|
||||
let mut paragraph = Paragraph::default();
|
||||
val.children.iter().for_each(|c| {
|
||||
parse_paragraph(&mut paragraph, c);
|
||||
parse_paragraph(&mut paragraph, c, cx);
|
||||
});
|
||||
node::Node::Paragraph(paragraph)
|
||||
}
|
||||
Node::MdxJsxFlowElement(val) => {
|
||||
let mut paragraph = Paragraph::default();
|
||||
val.children.iter().for_each(|c| {
|
||||
parse_paragraph(&mut paragraph, c);
|
||||
parse_paragraph(&mut paragraph, c, cx);
|
||||
});
|
||||
node::Node::Paragraph(paragraph)
|
||||
}
|
||||
|
|
@ -361,12 +403,44 @@ fn ast_to_node(value: mdast::Node, style: &TextViewStyle, cx: &mut App) -> node:
|
|||
.collect();
|
||||
val.children.iter().for_each(|c| {
|
||||
if let Node::TableRow(row) = c {
|
||||
parse_table_row(&mut table, row);
|
||||
parse_table_row(&mut table, row, cx);
|
||||
}
|
||||
});
|
||||
|
||||
node::Node::Table(table)
|
||||
}
|
||||
Node::FootnoteDefinition(def) => {
|
||||
let mut paragraph = Paragraph::default();
|
||||
let prefix = format!("[{}]: ", def.identifier);
|
||||
paragraph.push(InlineNode::new(&prefix).marks(vec![(
|
||||
0..prefix.len(),
|
||||
TextMark {
|
||||
italic: true,
|
||||
..Default::default()
|
||||
},
|
||||
)]));
|
||||
|
||||
def.children.iter().for_each(|c| {
|
||||
parse_paragraph(&mut paragraph, c, cx);
|
||||
});
|
||||
node::Node::Paragraph(paragraph)
|
||||
}
|
||||
Node::Definition(def) => {
|
||||
cx.add_ref(
|
||||
def.identifier.clone().into(),
|
||||
LinkMark {
|
||||
url: def.url.clone().into(),
|
||||
identifier: Some(def.identifier.clone().into()),
|
||||
title: def.title.clone().map(Into::into),
|
||||
},
|
||||
);
|
||||
|
||||
node::Node::Definition {
|
||||
identifier: def.identifier.clone().into(),
|
||||
url: def.url.clone().into(),
|
||||
title: def.title.clone().map(|s| s.into()),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::warn!("unsupported node: {:#?}", value);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::ops::Range;
|
||||
use std::{collections::HashMap, ops::Range};
|
||||
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder as _, px, relative, rems, AnyElement, App, DefiniteLength,
|
||||
|
|
@ -22,6 +22,8 @@ use super::{utils::list_item_prefix, TextViewStyle};
|
|||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct LinkMark {
|
||||
pub url: SharedString,
|
||||
/// Optional identifier for footnotes.
|
||||
pub identifier: Option<SharedString>,
|
||||
pub title: Option<SharedString>,
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +142,10 @@ impl InlineNode {
|
|||
pub(crate) struct Paragraph {
|
||||
pub(super) span: Option<Span>,
|
||||
pub(super) children: Vec<InlineNode>,
|
||||
/// The link references in this paragraph, used for reference links.
|
||||
///
|
||||
/// The key is the identifier, the value is the url.
|
||||
pub(super) link_refs: HashMap<SharedString, SharedString>,
|
||||
|
||||
pub(crate) state: InlineState,
|
||||
}
|
||||
|
|
@ -149,6 +155,7 @@ impl Paragraph {
|
|||
Self {
|
||||
span: None,
|
||||
children: vec![InlineNode::new(&text)],
|
||||
link_refs: HashMap::new(),
|
||||
state: InlineState::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -278,7 +285,7 @@ impl CodeBlock {
|
|||
code: SharedString,
|
||||
lang: Option<SharedString>,
|
||||
_: &TextViewStyle,
|
||||
cx: &mut App,
|
||||
cx: &App,
|
||||
) -> Self {
|
||||
let theme = cx.theme().highlight_theme.clone();
|
||||
let mut styles = vec![];
|
||||
|
|
@ -331,6 +338,19 @@ impl CodeBlock {
|
|||
}
|
||||
}
|
||||
|
||||
/// A context for rendering nodes, contains link references.
|
||||
#[derive(Default, Clone, PartialEq)]
|
||||
pub(crate) struct NodeContext {
|
||||
pub(crate) link_refs: HashMap<SharedString, LinkMark>,
|
||||
pub(crate) style: TextViewStyle,
|
||||
}
|
||||
|
||||
impl NodeContext {
|
||||
pub(super) fn add_ref(&mut self, identifier: SharedString, link: LinkMark) {
|
||||
self.link_refs.insert(identifier, link);
|
||||
}
|
||||
}
|
||||
|
||||
/// The AST Node of the rich text.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum Node {
|
||||
|
|
@ -362,6 +382,12 @@ pub(crate) enum Node {
|
|||
html: bool,
|
||||
},
|
||||
Divider,
|
||||
/// Use for to_markdown get raw definition
|
||||
Definition {
|
||||
identifier: SharedString,
|
||||
url: SharedString,
|
||||
title: Option<SharedString>,
|
||||
},
|
||||
Unknown,
|
||||
}
|
||||
|
||||
|
|
@ -464,7 +490,7 @@ impl Node {
|
|||
text.push('\n');
|
||||
}
|
||||
}
|
||||
Node::Break { .. } | Node::Divider | Node::Unknown => {}
|
||||
Node::Definition { .. } | Node::Break { .. } | Node::Divider | Node::Unknown => {}
|
||||
}
|
||||
|
||||
text
|
||||
|
|
@ -472,7 +498,12 @@ impl Node {
|
|||
}
|
||||
|
||||
impl Paragraph {
|
||||
fn render(&self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
fn render(
|
||||
&self,
|
||||
node_cx: &NodeContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let span = self.span;
|
||||
let children = &self.children;
|
||||
|
||||
|
|
@ -547,14 +578,21 @@ impl Paragraph {
|
|||
highlight.background_color = Some(cx.theme().accent);
|
||||
}
|
||||
|
||||
if let Some(link_mark) = &style.link {
|
||||
if let Some(mut link_mark) = style.link.clone() {
|
||||
highlight.color = Some(cx.theme().link);
|
||||
highlight.underline = Some(gpui::UnderlineStyle {
|
||||
thickness: gpui::px(1.),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
links.push((inner_range.clone(), link_mark.clone()));
|
||||
// convert link references, replace link
|
||||
if let Some(identifier) = link_mark.identifier.as_ref() {
|
||||
if let Some(mark) = node_cx.link_refs.get(identifier) {
|
||||
link_mark = mark.clone();
|
||||
}
|
||||
}
|
||||
|
||||
links.push((inner_range.clone(), link_mark));
|
||||
}
|
||||
|
||||
node_highlights.push((inner_range, highlight));
|
||||
|
|
@ -589,7 +627,7 @@ impl Node {
|
|||
item: &Node,
|
||||
ix: usize,
|
||||
state: ListState,
|
||||
text_view_style: &TextViewStyle,
|
||||
node_cx: &NodeContext,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
|
|
@ -617,7 +655,7 @@ impl Node {
|
|||
}),
|
||||
false,
|
||||
true,
|
||||
text_view_style,
|
||||
node_cx,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
|
@ -681,7 +719,7 @@ impl Node {
|
|||
}),
|
||||
true,
|
||||
true,
|
||||
text_view_style,
|
||||
node_cx,
|
||||
window,
|
||||
cx,
|
||||
)))
|
||||
|
|
@ -696,7 +734,12 @@ impl Node {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_table(item: &Node, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
fn render_table(
|
||||
item: &Node,
|
||||
node_cx: &NodeContext,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
const DEFAULT_LENGTH: usize = 5;
|
||||
const MAX_LENGTH: usize = 150;
|
||||
let col_lens = match item {
|
||||
|
|
@ -767,7 +810,7 @@ impl Node {
|
|||
.border_color(cx.theme().border)
|
||||
})
|
||||
.truncate()
|
||||
.child(cell.children.render(window, cx)),
|
||||
.child(cell.children.render(node_cx, window, cx)),
|
||||
)
|
||||
}
|
||||
cells
|
||||
|
|
@ -786,7 +829,7 @@ impl Node {
|
|||
list_state: Option<ListState>,
|
||||
is_root: bool,
|
||||
is_last_child: bool,
|
||||
style: &TextViewStyle,
|
||||
node_cx: &NodeContext,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
|
|
@ -794,7 +837,7 @@ impl Node {
|
|||
let mb = if in_list || is_last_child {
|
||||
rems(0.)
|
||||
} else {
|
||||
style.paragraph_gap
|
||||
node_cx.style.paragraph_gap
|
||||
};
|
||||
|
||||
match self {
|
||||
|
|
@ -804,14 +847,14 @@ impl Node {
|
|||
let children_len = children.len();
|
||||
children.into_iter().enumerate().map(move |(index, c)| {
|
||||
let is_last_child = is_root && index == children_len - 1;
|
||||
c.render(None, false, is_last_child, style, window, cx)
|
||||
c.render(None, false, is_last_child, node_cx, window, cx)
|
||||
})
|
||||
})
|
||||
.into_any_element(),
|
||||
Node::Paragraph(paragraph) => div()
|
||||
.id("p")
|
||||
.mb(mb)
|
||||
.child(paragraph.render(window, cx))
|
||||
.child(paragraph.render(node_cx, window, cx))
|
||||
.into_any_element(),
|
||||
Node::Heading { level, children } => {
|
||||
let (text_size, font_weight) = match level {
|
||||
|
|
@ -824,7 +867,7 @@ impl Node {
|
|||
_ => (rems(1.), FontWeight::NORMAL),
|
||||
};
|
||||
|
||||
let text_size = text_size.to_pixels(style.heading_base_font_size);
|
||||
let text_size = text_size.to_pixels(node_cx.style.heading_base_font_size);
|
||||
|
||||
h_flex()
|
||||
.id(("h", *level as usize))
|
||||
|
|
@ -832,7 +875,7 @@ impl Node {
|
|||
.whitespace_normal()
|
||||
.text_size(text_size)
|
||||
.font_weight(font_weight)
|
||||
.child(children.render(window, cx))
|
||||
.child(children.render(node_cx, window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
Node::Blockquote { children } => div()
|
||||
|
|
@ -847,7 +890,7 @@ impl Node {
|
|||
let children_len = children.len();
|
||||
children.into_iter().enumerate().map(move |(index, c)| {
|
||||
let is_last_child = is_root && index == children_len - 1;
|
||||
c.render(None, false, is_last_child, style, window, cx)
|
||||
c.render(None, false, is_last_child, node_cx, window, cx)
|
||||
})
|
||||
})
|
||||
.into_any_element(),
|
||||
|
|
@ -869,7 +912,7 @@ impl Node {
|
|||
todo: list_state.todo,
|
||||
depth: list_state.depth,
|
||||
},
|
||||
style,
|
||||
node_cx,
|
||||
window,
|
||||
cx,
|
||||
));
|
||||
|
|
@ -882,7 +925,7 @@ impl Node {
|
|||
})
|
||||
.into_any_element(),
|
||||
Node::CodeBlock(code_block) => code_block.render(mb, window, cx),
|
||||
Node::Table { .. } => Self::render_table(&self, window, cx).into_any_element(),
|
||||
Node::Table { .. } => Self::render_table(&self, node_cx, window, cx).into_any_element(),
|
||||
Node::Divider => div()
|
||||
.id("divider")
|
||||
.bg(cx.theme().border)
|
||||
|
|
@ -890,6 +933,7 @@ impl Node {
|
|||
.mb(mb)
|
||||
.into_any_element(),
|
||||
Node::Break { .. } => div().id("break").into_any_element(),
|
||||
Node::Unknown | Node::Definition { .. } => div().into_any_element(),
|
||||
_ => {
|
||||
if cfg!(debug_assertions) {
|
||||
tracing::warn!("unknown implementation: {:?}", self);
|
||||
|
|
@ -1065,6 +1109,17 @@ impl Node {
|
|||
}
|
||||
}
|
||||
Node::Divider => "---".to_string(),
|
||||
Node::Definition {
|
||||
identifier,
|
||||
url,
|
||||
title,
|
||||
} => {
|
||||
if let Some(title) = title {
|
||||
format!("[{}]: {} \"{}\"", identifier, url, title)
|
||||
} else {
|
||||
format!("[{}]: {}", identifier, url)
|
||||
}
|
||||
}
|
||||
Node::Unknown => "".to_string(),
|
||||
}
|
||||
.trim()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
global_state::GlobalState,
|
||||
highlighter::HighlightTheme,
|
||||
input::{self},
|
||||
text::node::{self},
|
||||
text::node::{self, NodeContext},
|
||||
};
|
||||
|
||||
const CONTEXT: &'static str = "TextView";
|
||||
|
|
@ -68,10 +68,10 @@ pub struct TextView {
|
|||
#[derive(Default, Clone, PartialEq)]
|
||||
pub(crate) struct TextViewState {
|
||||
root: Option<Result<Rc<node::Node>, SharedString>>,
|
||||
pub(crate) node_cx: Rc<node::NodeContext>,
|
||||
|
||||
raw: SharedString,
|
||||
focus_handle: Option<FocusHandle>,
|
||||
style: TextViewStyle,
|
||||
|
||||
/// The bounds of the text view
|
||||
bounds: Bounds<Pixels>,
|
||||
|
|
@ -92,7 +92,7 @@ impl TextViewState {
|
|||
raw: SharedString::default(),
|
||||
focus_handle: Some(focus_handle),
|
||||
root: None,
|
||||
style: TextViewStyle::default(),
|
||||
node_cx: Rc::new(NodeContext::default()),
|
||||
_last_parsed: None,
|
||||
bounds: Bounds::default(),
|
||||
selection_positions: (None, None),
|
||||
|
|
@ -116,7 +116,7 @@ impl TextViewState {
|
|||
style: &TextViewStyle,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let is_changed = self.raw != new_text || self.style != *style;
|
||||
let is_changed = self.raw != new_text || self.node_cx.style != *style;
|
||||
|
||||
if self.root.is_some() && !is_changed {
|
||||
return;
|
||||
|
|
@ -128,20 +128,22 @@ impl TextViewState {
|
|||
}
|
||||
}
|
||||
|
||||
let mut node_cx = NodeContext::default();
|
||||
node_cx.style = style.clone();
|
||||
self.raw = new_text;
|
||||
// NOTE: About 100ms
|
||||
// let measure = crate::Measure::new("parse_markdown");
|
||||
self.root = Some(
|
||||
if is_html {
|
||||
super::format::html::parse(&self.raw)
|
||||
super::format::html::parse(&self.raw, &mut node_cx)
|
||||
} else {
|
||||
super::format::markdown::parse(&self.raw, &style, cx)
|
||||
super::format::markdown::parse(&self.raw, &style, &mut node_cx, cx)
|
||||
}
|
||||
.map(Rc::new),
|
||||
);
|
||||
self.node_cx = Rc::new(node_cx);
|
||||
// measure.end();
|
||||
self._last_parsed = Some(Instant::now());
|
||||
self.style = style.clone();
|
||||
self.clear_selection();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue