Skip to main content

gpui_base/text/
markdown_ext.rs

1use std::{
2    any::Any,
3    collections::HashMap,
4    fmt,
5    sync::{
6        Arc,
7        atomic::{AtomicU64, Ordering},
8    },
9};
10
11use gpui::{AnyElement, App, IntoElement, SharedString, Window};
12use markdown::{ParseOptions, mdast};
13
14use crate::text::node::Span;
15
16static MARKDOWN_EXTENSIONS_REVISION: AtomicU64 = AtomicU64::new(1);
17
18/// Re-export of the Markdown AST types used by custom parsers.
19pub use markdown::mdast as markdown_ast;
20
21/// Type for a custom Markdown block parser.
22///
23/// Parsers run during Markdown AST conversion, often on a background task. They
24/// must not depend on [`Window`] or [`App`]; return parsed, reusable data in a
25/// [`MarkdownNode`] and render it later with a block renderer.
26pub type MarkdownBlockParserFn =
27    dyn for<'a> Fn(&mdast::Node, &MarkdownParseContext<'a>) -> Option<MarkdownNode> + Send + Sync;
28
29/// Type for a custom Markdown block renderer.
30pub type MarkdownBlockRenderFn =
31    dyn Fn(&MarkdownNode, &mut Window, &mut App) -> AnyElement + Send + Sync;
32
33/// A reusable Markdown extension that parses and renders one custom node.
34pub trait MarkdownPlugin: Send + Sync + 'static {
35    /// Whether this plugin produces block-level nodes.
36    ///
37    /// Plugins are inline by default. TextView does not support inline custom
38    /// Markdown rendering yet, so block plugins should return `true`.
39    fn is_block(&self) -> bool {
40        false
41    }
42
43    /// Stable name for nodes produced by this plugin.
44    fn name(&self) -> &str;
45
46    /// Convert an mdast node into a custom Markdown node.
47    fn parse(&self, node: &mdast::Node, cx: &MarkdownParseContext<'_>) -> Option<MarkdownNode>;
48
49    /// Render a custom Markdown node produced by this plugin.
50    fn render(&self, node: &MarkdownNode, window: &mut Window, cx: &mut App) -> impl IntoElement;
51}
52
53/// Context passed to custom Markdown parsers.
54pub struct MarkdownParseContext<'a> {
55    source: &'a str,
56    offset: usize,
57}
58
59impl<'a> MarkdownParseContext<'a> {
60    pub(crate) fn new(source: &'a str, offset: usize) -> Self {
61        Self { source, offset }
62    }
63
64    /// Source text for the Markdown fragment currently being parsed.
65    pub fn source(&self) -> &'a str {
66        self.source
67    }
68
69    /// Byte offset of `source` in the full document when parsing an appended
70    /// fragment.
71    pub fn offset(&self) -> usize {
72        self.offset
73    }
74
75    /// Source slice for a specific mdast node.
76    pub fn node_source(&self, node: &mdast::Node) -> Option<&'a str> {
77        let position = node.position()?;
78        self.source.get(position.start.offset..position.end.offset)
79    }
80}
81
82/// A custom Markdown node produced by [`MarkdownExtensions`].
83#[derive(Clone)]
84pub struct MarkdownNode {
85    name: SharedString,
86    text: SharedString,
87    markdown: SharedString,
88    data: Arc<dyn Any + Send + Sync>,
89    pub(crate) span: Option<Span>,
90}
91
92impl MarkdownNode {
93    /// Create a custom Markdown node with a stable name and typed data.
94    pub fn new<T>(name: impl Into<SharedString>, data: T) -> Self
95    where
96        T: Any + Send + Sync + 'static,
97    {
98        Self {
99            name: name.into(),
100            text: SharedString::default(),
101            markdown: SharedString::default(),
102            data: Arc::new(data),
103            span: None,
104        }
105    }
106
107    /// Stable name for this custom node.
108    pub fn name(&self) -> &str {
109        &self.name
110    }
111
112    /// Text representation of this custom node.
113    pub fn as_text(&self) -> &str {
114        &self.text
115    }
116
117    /// Markdown representation of this custom node.
118    pub fn as_markdown(&self) -> &str {
119        &self.markdown
120    }
121
122    /// Set the text representation of this custom node.
123    pub fn text(mut self, text: impl Into<SharedString>) -> Self {
124        self.text = text.into();
125        self
126    }
127
128    /// Set the Markdown representation of this custom node.
129    pub fn markdown(mut self, markdown: impl Into<SharedString>) -> Self {
130        self.markdown = markdown.into();
131        self
132    }
133
134    /// Read typed data.
135    pub fn data<T>(&self) -> Option<&T>
136    where
137        T: Any + Send + Sync + 'static,
138    {
139        self.data.downcast_ref()
140    }
141
142    pub(crate) fn set_span(&mut self, span: Option<Span>) {
143        self.span = span;
144    }
145
146    pub(crate) fn to_markdown(&self) -> String {
147        if self.markdown.is_empty() {
148            self.text.to_string()
149        } else {
150            self.markdown.to_string()
151        }
152    }
153}
154
155impl fmt::Debug for MarkdownNode {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        f.debug_struct("MarkdownNode")
158            .field("name", &self.name)
159            .field("text", &self.text)
160            .field("markdown", &self.markdown)
161            .field("span", &self.span)
162            .finish_non_exhaustive()
163    }
164}
165
166impl PartialEq for MarkdownNode {
167    fn eq(&self, other: &Self) -> bool {
168        self.name == other.name
169            && self.text == other.text
170            && self.markdown == other.markdown
171            && self.span == other.span
172    }
173}
174
175/// Registry for custom Markdown parsing and rendering.
176#[derive(Clone, Default)]
177pub struct MarkdownExtensions {
178    enable_mdx: bool,
179    block_parsers: Vec<Arc<MarkdownBlockParserFn>>,
180    block_renderers: HashMap<SharedString, Arc<MarkdownBlockRenderFn>>,
181    revision: u64,
182}
183
184impl MarkdownExtensions {
185    /// Enable MDX JSX/expression constructs.
186    ///
187    /// This disables raw HTML constructs because `markdown-rs` gives HTML
188    /// priority over MDX when both are enabled.
189    pub fn mdx(mut self) -> Self {
190        self.enable_mdx = true;
191        self.bump_revision();
192        self
193    }
194
195    /// Register a parser for block-level Markdown AST nodes.
196    pub fn block_parser<F>(mut self, parser: F) -> Self
197    where
198        F: for<'a> Fn(&mdast::Node, &MarkdownParseContext<'a>) -> Option<MarkdownNode>
199            + Send
200            + Sync
201            + 'static,
202    {
203        self.push_block_parser(parser);
204        self
205    }
206
207    /// Register a renderer for a custom block node name.
208    pub fn block_renderer<F, E>(mut self, name: impl Into<SharedString>, renderer: F) -> Self
209    where
210        F: Fn(&MarkdownNode, &mut Window, &mut App) -> E + Send + Sync + 'static,
211        E: IntoElement,
212    {
213        self.push_block_renderer(name, renderer);
214        self
215    }
216
217    /// Apply a reusable Markdown plugin.
218    pub fn plugin<P>(self, plugin: P) -> Self
219    where
220        P: MarkdownPlugin,
221    {
222        let plugin = Arc::new(plugin);
223        let name = SharedString::from(plugin.name().to_string());
224        let parser = plugin.clone();
225        let renderer = plugin;
226
227        if parser.is_block() {
228            let mut extensions = self.block_parser(move |node, cx| parser.parse(node, cx));
229            extensions.push_block_renderer(name, move |node, window, cx| {
230                renderer.render(node, window, cx).into_any_element()
231            });
232            extensions
233        } else {
234            panic!("inline Markdown plugins are not supported by TextView yet")
235        }
236    }
237
238    pub(crate) fn revision(&self) -> u64 {
239        self.revision
240    }
241
242    /// Whether replacing these extension handles can change the parsed tree.
243    ///
244    /// Render methods commonly rebuild equivalent plugin closures every frame.
245    /// Their globally unique revisions differ, but the parser shape remains
246    /// stable; render handles may be refreshed without reparsing the document.
247    pub(crate) fn has_same_parser_configuration(&self, other: &Self) -> bool {
248        self.enable_mdx == other.enable_mdx
249            && self.block_parsers.len() == other.block_parsers.len()
250            && self.block_renderers.len() == other.block_renderers.len()
251            && self
252                .block_renderers
253                .keys()
254                .all(|name| other.block_renderers.contains_key(name))
255    }
256
257    pub(crate) fn push_block_parser<F>(&mut self, parser: F)
258    where
259        F: for<'a> Fn(&mdast::Node, &MarkdownParseContext<'a>) -> Option<MarkdownNode>
260            + Send
261            + Sync
262            + 'static,
263    {
264        self.block_parsers.push(Arc::new(parser));
265        self.bump_revision();
266    }
267
268    pub(crate) fn push_block_renderer<F, E>(&mut self, name: impl Into<SharedString>, renderer: F)
269    where
270        F: Fn(&MarkdownNode, &mut Window, &mut App) -> E + Send + Sync + 'static,
271        E: IntoElement,
272    {
273        self.block_renderers.insert(
274            name.into(),
275            Arc::new(move |node, window, cx| renderer(node, window, cx).into_any_element()),
276        );
277        self.bump_revision();
278    }
279
280    pub(crate) fn parse_options(&self) -> ParseOptions {
281        let mut options = ParseOptions::gfm();
282        if self.enable_mdx {
283            options.constructs.html_flow = false;
284            options.constructs.html_text = false;
285            options.constructs.mdx_expression_flow = true;
286            options.constructs.mdx_expression_text = true;
287            options.constructs.mdx_jsx_flow = true;
288            options.constructs.mdx_jsx_text = true;
289        }
290        options
291    }
292
293    pub(crate) fn parse_block(
294        &self,
295        node: &mdast::Node,
296        cx: &MarkdownParseContext<'_>,
297    ) -> Option<MarkdownNode> {
298        for parser in &self.block_parsers {
299            if let Some(node) = parser(node, cx) {
300                return Some(node);
301            }
302        }
303        None
304    }
305
306    pub(crate) fn render_block(
307        &self,
308        node: &MarkdownNode,
309        window: &mut Window,
310        cx: &mut App,
311    ) -> Option<AnyElement> {
312        self.block_renderers
313            .get(node.name())
314            .map(|render| render(node, window, cx))
315    }
316
317    fn bump_revision(&mut self) {
318        self.revision = MARKDOWN_EXTENSIONS_REVISION.fetch_add(1, Ordering::Relaxed);
319    }
320}