markdown: Add to support LinkReference and Footnote. (#1201)

This commit is contained in:
Jason Lee 2025-09-03 16:39:17 +08:00 committed by GitHub
parent f4a292d975
commit ee40f65fb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 232 additions and 81 deletions

View file

@ -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

View file

@ -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;
};

View file

@ -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 {

View file

@ -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);

View file

@ -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()

View file

@ -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();
}