1use std::{
2 any::Any,
3 collections::HashMap,
4 fmt,
5 ops::Range,
6 sync::{
7 Arc,
8 atomic::{AtomicU64, Ordering},
9 },
10};
11
12use gpui::{AnyElement, App, IntoElement, SharedString, Window};
13use markdown::{ParseOptions, mdast};
14
15use super::{InlineElement, InlineRenderContext};
16use crate::text::node::Span;
17
18static MARKDOWN_EXTENSIONS_REVISION: AtomicU64 = AtomicU64::new(1);
19
20pub use markdown::mdast as markdown_ast;
22
23pub type MarkdownBlockParserFn =
29 dyn for<'a> Fn(&mdast::Node, &MarkdownParseContext<'a>) -> Option<MarkdownNode> + Send + Sync;
30
31pub type MarkdownBlockRenderFn =
33 dyn Fn(&MarkdownNode, &mut Window, &mut App) -> AnyElement + Send + Sync;
34
35type MarkdownInlineParserFn = MarkdownBlockParserFn;
37
38type MarkdownInlineRenderFn = dyn Fn(&MarkdownNode, &InlineRenderContext, &mut Window, &mut App) -> Option<InlineElement>
41 + Send
42 + Sync;
43
44pub trait MarkdownPlugin: Send + Sync + 'static {
46 fn is_block(&self) -> bool {
50 false
51 }
52
53 fn name(&self) -> &str;
55
56 fn parse(&self, node: &mdast::Node, cx: &MarkdownParseContext<'_>) -> Option<MarkdownNode>;
58
59 fn render(&self, node: &MarkdownNode, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
61 node.as_text().to_string()
62 }
63
64 fn render_inline(
67 &self,
68 node: &MarkdownNode,
69 _context: &InlineRenderContext,
70 window: &mut Window,
71 cx: &mut App,
72 ) -> Option<InlineElement> {
73 Some(InlineElement::new(self.render(node, window, cx)))
74 }
75}
76
77pub struct MarkdownParseContext<'a> {
79 source: &'a str,
80 offset: usize,
81}
82
83impl<'a> MarkdownParseContext<'a> {
84 pub(crate) fn new(source: &'a str, offset: usize) -> Self {
85 Self { source, offset }
86 }
87
88 pub fn source(&self) -> &'a str {
90 self.source
91 }
92
93 pub fn offset(&self) -> usize {
96 self.offset
97 }
98
99 pub fn node_source(&self, node: &mdast::Node) -> Option<&'a str> {
101 let position = node.position()?;
102 self.source.get(position.start.offset..position.end.offset)
103 }
104}
105
106#[derive(Clone)]
108pub struct MarkdownNode {
109 name: SharedString,
110 text: SharedString,
111 markdown: SharedString,
112 accessibility_label: Option<SharedString>,
113 data: Arc<dyn Any + Send + Sync>,
114 pub(crate) span: Option<Span>,
115}
116
117impl MarkdownNode {
118 pub fn new<T>(name: impl Into<SharedString>, data: T) -> Self
120 where
121 T: Any + Send + Sync + 'static,
122 {
123 Self {
124 name: name.into(),
125 text: SharedString::default(),
126 markdown: SharedString::default(),
127 accessibility_label: None,
128 data: Arc::new(data),
129 span: None,
130 }
131 }
132
133 pub fn name(&self) -> &str {
135 &self.name
136 }
137
138 pub fn as_text(&self) -> &str {
140 &self.text
141 }
142
143 pub fn as_markdown(&self) -> &str {
145 &self.markdown
146 }
147
148 pub fn source_range(&self) -> Option<Range<usize>> {
150 self.span.map(|span| span.start..span.end)
151 }
152
153 pub fn accessibility_name(&self) -> &str {
155 self.accessibility_label.as_deref().unwrap_or(&self.text)
156 }
157
158 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
159 let label = label.into();
160 self.accessibility_label = (!label.is_empty()).then_some(label);
161 self
162 }
163
164 pub(crate) fn shared_text(&self) -> SharedString {
170 self.text.clone()
171 }
172
173 pub(crate) fn shared_accessibility_name(&self) -> SharedString {
175 self.accessibility_label
176 .clone()
177 .unwrap_or_else(|| self.text.clone())
178 }
179
180 pub(crate) fn with_inline_source(mut self, source: &str) -> Self {
181 if self.text.is_empty() {
182 self.text = source.to_string().into();
183 }
184 if self.markdown.is_empty() {
185 self.markdown = source.to_string().into();
186 }
187 self
188 }
189
190 pub fn text(mut self, text: impl Into<SharedString>) -> Self {
192 self.text = text.into();
193 self
194 }
195
196 pub fn markdown(mut self, markdown: impl Into<SharedString>) -> Self {
198 self.markdown = markdown.into();
199 self
200 }
201
202 pub fn data<T>(&self) -> Option<&T>
204 where
205 T: Any + Send + Sync + 'static,
206 {
207 self.data.downcast_ref()
208 }
209
210 pub(crate) fn set_span(&mut self, span: Option<Span>) {
211 self.span = span;
212 }
213
214 pub(crate) fn to_markdown(&self) -> String {
215 if self.markdown.is_empty() {
216 self.text.to_string()
217 } else {
218 self.markdown.to_string()
219 }
220 }
221}
222
223impl fmt::Debug for MarkdownNode {
224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225 f.debug_struct("MarkdownNode")
226 .field("name", &self.name)
227 .field("text", &self.text)
228 .field("markdown", &self.markdown)
229 .field("span", &self.span)
230 .finish_non_exhaustive()
231 }
232}
233
234impl PartialEq for MarkdownNode {
235 fn eq(&self, other: &Self) -> bool {
236 self.name == other.name
237 && self.text == other.text
238 && self.markdown == other.markdown
239 && self.accessibility_label == other.accessibility_label
240 && self.span == other.span
241 }
242}
243
244#[derive(Clone, Default)]
246pub struct MarkdownExtensions {
247 enable_mdx: bool,
248 enable_frontmatter: bool,
249 block_parsers: Vec<Arc<MarkdownBlockParserFn>>,
250 block_renderers: HashMap<SharedString, Arc<MarkdownBlockRenderFn>>,
251 inline_parsers: Vec<Arc<MarkdownInlineParserFn>>,
252 inline_renderers: HashMap<SharedString, Arc<MarkdownInlineRenderFn>>,
253 revision: u64,
254 parser_revision: u64,
255}
256
257impl MarkdownExtensions {
258 pub fn parser_revision(mut self, revision: u64) -> Self {
262 self.parser_revision = revision;
263 self.bump_revision();
264 self
265 }
266
267 pub fn frontmatter(mut self) -> Self {
273 self.enable_frontmatter = true;
274 self.bump_revision();
275 self
276 }
277
278 pub fn mdx(mut self) -> Self {
283 self.enable_mdx = true;
284 self.bump_revision();
285 self
286 }
287
288 pub fn block_parser<F>(mut self, parser: F) -> Self
290 where
291 F: for<'a> Fn(&mdast::Node, &MarkdownParseContext<'a>) -> Option<MarkdownNode>
292 + Send
293 + Sync
294 + 'static,
295 {
296 self.push_block_parser(parser);
297 self
298 }
299
300 pub fn block_renderer<F, E>(mut self, name: impl Into<SharedString>, renderer: F) -> Self
302 where
303 F: Fn(&MarkdownNode, &mut Window, &mut App) -> E + Send + Sync + 'static,
304 E: IntoElement,
305 {
306 self.push_block_renderer(name, renderer);
307 self
308 }
309
310 pub fn plugin<P>(self, plugin: P) -> Self
312 where
313 P: MarkdownPlugin,
314 {
315 let plugin = Arc::new(plugin);
316 let name = SharedString::from(plugin.name().to_string());
317 let parser = plugin.clone();
318 let renderer = plugin;
319
320 if parser.is_block() {
321 let mut extensions = self.block_parser(move |node, cx| parser.parse(node, cx));
322 extensions.push_block_renderer(name, move |node, window, cx| {
323 renderer.render(node, window, cx).into_any_element()
324 });
325 extensions
326 } else {
327 let mut extensions = self;
328 extensions
329 .inline_parsers
330 .push(Arc::new(move |node, cx| parser.parse(node, cx)));
331 extensions.inline_renderers.insert(
332 name,
333 Arc::new(move |node, context, window, cx| {
334 renderer.render_inline(node, context, window, cx)
335 }),
336 );
337 extensions.bump_revision();
338 extensions
339 }
340 }
341
342 pub(crate) fn revision(&self) -> u64 {
343 self.revision
344 }
345
346 pub(crate) fn has_same_parser_configuration(&self, other: &Self) -> bool {
352 self.parser_revision == other.parser_revision
353 && self.enable_mdx == other.enable_mdx
354 && self.enable_frontmatter == other.enable_frontmatter
355 && self.block_parsers.len() == other.block_parsers.len()
356 && self.block_renderers.len() == other.block_renderers.len()
357 && self.inline_parsers.len() == other.inline_parsers.len()
358 && self.inline_renderers.len() == other.inline_renderers.len()
359 && self
360 .inline_renderers
361 .keys()
362 .all(|name| other.inline_renderers.contains_key(name))
363 && self
364 .block_renderers
365 .keys()
366 .all(|name| other.block_renderers.contains_key(name))
367 }
368
369 pub(crate) fn push_block_parser<F>(&mut self, parser: F)
370 where
371 F: for<'a> Fn(&mdast::Node, &MarkdownParseContext<'a>) -> Option<MarkdownNode>
372 + Send
373 + Sync
374 + 'static,
375 {
376 self.block_parsers.push(Arc::new(parser));
377 self.bump_revision();
378 }
379
380 pub(crate) fn push_block_renderer<F, E>(&mut self, name: impl Into<SharedString>, renderer: F)
381 where
382 F: Fn(&MarkdownNode, &mut Window, &mut App) -> E + Send + Sync + 'static,
383 E: IntoElement,
384 {
385 self.block_renderers.insert(
386 name.into(),
387 Arc::new(move |node, window, cx| renderer(node, window, cx).into_any_element()),
388 );
389 self.bump_revision();
390 }
391
392 pub(crate) fn parse_options(&self) -> ParseOptions {
393 let mut options = ParseOptions::gfm();
394 options.constructs.frontmatter = self.enable_frontmatter;
395 options.constructs.math_text = true;
396 options.constructs.math_flow = true;
400 if self.enable_mdx {
401 options.constructs.html_flow = false;
402 options.constructs.html_text = false;
403 options.constructs.mdx_expression_flow = true;
404 options.constructs.mdx_expression_text = true;
405 options.constructs.mdx_jsx_flow = true;
406 options.constructs.mdx_jsx_text = true;
407 }
408 options
409 }
410
411 pub(crate) fn parse_block(
412 &self,
413 node: &mdast::Node,
414 cx: &MarkdownParseContext<'_>,
415 ) -> Option<MarkdownNode> {
416 for parser in &self.block_parsers {
417 if let Some(node) = parser(node, cx) {
418 return Some(node);
419 }
420 }
421 None
422 }
423
424 pub(crate) fn parse_inline(
425 &self,
426 node: &mdast::Node,
427 cx: &MarkdownParseContext<'_>,
428 ) -> Option<MarkdownNode> {
429 self.inline_parsers
430 .iter()
431 .find_map(|parser| parser(node, cx))
432 }
433
434 pub(crate) fn render_inline(
435 &self,
436 node: &MarkdownNode,
437 context: &InlineRenderContext,
438 window: &mut Window,
439 cx: &mut App,
440 ) -> Option<InlineElement> {
441 self.inline_renderers
442 .get(node.name())
443 .and_then(|render| render(node, context, window, cx))
444 }
445
446 pub(crate) fn render_block(
447 &self,
448 node: &MarkdownNode,
449 window: &mut Window,
450 cx: &mut App,
451 ) -> Option<AnyElement> {
452 self.block_renderers
453 .get(node.name())
454 .map(|render| render(node, window, cx))
455 }
456
457 fn bump_revision(&mut self) {
458 self.revision = MARKDOWN_EXTENSIONS_REVISION.fetch_add(1, Ordering::Relaxed);
459 }
460}
461
462#[cfg(test)]
464pub(super) struct TestInlinePlugin {
465 name: &'static str,
466 parser: Option<Arc<MarkdownInlineParserFn>>,
467 renderer: Option<Arc<MarkdownInlineRenderFn>>,
468}
469
470#[cfg(test)]
471impl TestInlinePlugin {
472 pub(super) fn new(name: &'static str) -> Self {
473 Self {
474 name,
475 parser: None,
476 renderer: None,
477 }
478 }
479
480 pub(super) fn parse_with<F>(mut self, parser: F) -> Self
481 where
482 F: for<'a> Fn(&mdast::Node, &MarkdownParseContext<'a>) -> Option<MarkdownNode>
483 + Send
484 + Sync
485 + 'static,
486 {
487 self.parser = Some(Arc::new(parser));
488 self
489 }
490
491 pub(super) fn render_with<F>(mut self, renderer: F) -> Self
492 where
493 F: Fn(&MarkdownNode, &InlineRenderContext, &mut Window, &mut App) -> Option<InlineElement>
494 + Send
495 + Sync
496 + 'static,
497 {
498 self.renderer = Some(Arc::new(renderer));
499 self
500 }
501}
502
503#[cfg(test)]
504impl MarkdownPlugin for TestInlinePlugin {
505 fn name(&self) -> &str {
506 self.name
507 }
508
509 fn parse(&self, node: &mdast::Node, cx: &MarkdownParseContext<'_>) -> Option<MarkdownNode> {
510 self.parser.as_ref().and_then(|parse| parse(node, cx))
511 }
512
513 fn render_inline(
514 &self,
515 node: &MarkdownNode,
516 context: &InlineRenderContext,
517 window: &mut Window,
518 cx: &mut App,
519 ) -> Option<InlineElement> {
520 self.renderer
521 .as_ref()
522 .and_then(|render| render(node, context, window, cx))
523 }
524}