diff --git a/crates/story/examples/fixtures/test.md b/crates/story/examples/fixtures/test.md index e216a476..464000db 100644 --- a/crates/story/examples/fixtures/test.md +++ b/crates/story/examples/fixtures/test.md @@ -2,13 +2,19 @@ Build Status [![Build Status](https://github.com/longbridge/gpui-component/actions/workflows/ci.yml/badge.svg)](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. ![Img](https://miro.medium.com/v2/resize:fit:1400/format:webp/1*WgEz5f3n3lD7MfC7NeQGOA.jpeg) -这是一个中文演示段落,用于展示更多的 [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 diff --git a/crates/ui/src/highlighter/highlighter.rs b/crates/ui/src/highlighter/highlighter.rs index 2d68d9a9..09fa5eb2 100644 --- a/crates/ui/src/highlighter/highlighter.rs +++ b/crates/ui/src/highlighter/highlighter.rs @@ -279,7 +279,7 @@ impl SyntaxHighlighter { selected_range: &Range, 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; }; diff --git a/crates/ui/src/text/format/html.rs b/crates/ui/src/text/format/html.rs index 960589f3..a9513c0c 100644 --- a/crates/ui/src/text/format/html.rs +++ b/crates/ui/src/text/format/html.rs @@ -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 { +pub(crate) fn parse(source: &str, cx: &mut NodeContext) -> Result { let opts = ParseOpts { ..Default::default() }; @@ -77,7 +77,8 @@ pub(crate) fn parse(source: &str) -> Result { 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, paragraph: &mut Paragraph) -> Option { +fn parse_node( + node: &Rc, + paragraph: &mut Paragraph, + cx: &mut NodeContext, +) -> Option { match node.data { NodeData::Text { ref contents } => { let text = contents.borrow().to_string(); @@ -495,7 +503,7 @@ fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> Option } 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, paragraph: &mut Paragraph) -> Option 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, paragraph: &mut Paragraph) -> Option } } 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, paragraph: &mut Paragraph) -> Option // 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, paragraph: &mut Paragraph) -> Option } }, 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, paragraph: &mut Paragraph) -> Option } } -fn consume_children_nodes(node: &Node, paragraph: &mut Paragraph) -> Vec { +fn consume_children_nodes( + node: &Node, + paragraph: &mut Paragraph, + cx: &mut NodeContext, +) -> Vec { 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, 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#"

and code text

"#; - 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 { "#; - 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#"Example"#; - 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#"Example"#; - let node = super::parse(html).unwrap(); + let node = super::parse(html, &mut cx).unwrap(); assert_eq!( node, Node::Paragraph(Paragraph { diff --git a/crates/ui/src/text/format/markdown.rs b/crates/ui/src/text/format/markdown.rs index a46466ac..68c62eea 100644 --- a/crates/ui/src/text/format/markdown.rs +++ b/crates/ui/src/text/format/markdown.rs @@ -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 { 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); diff --git a/crates/ui/src/text/node.rs b/crates/ui/src/text/node.rs index 71bc319f..82ff7400 100644 --- a/crates/ui/src/text/node.rs +++ b/crates/ui/src/text/node.rs @@ -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, pub title: Option, } @@ -140,6 +142,10 @@ impl InlineNode { pub(crate) struct Paragraph { pub(super) span: Option, pub(super) children: Vec, + /// 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, 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, _: &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, + 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, + }, 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, 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() diff --git a/crates/ui/src/text/text_view.rs b/crates/ui/src/text/text_view.rs index d96a63b9..a0fd54de 100644 --- a/crates/ui/src/text/text_view.rs +++ b/crates/ui/src/text/text_view.rs @@ -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, SharedString>>, + pub(crate) node_cx: Rc, raw: SharedString, focus_handle: Option, - style: TextViewStyle, /// The bounds of the text view bounds: Bounds, @@ -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(); }