markdown.rs

   1pub mod parser;
   2
   3use std::collections::{HashMap, HashSet};
   4use std::iter;
   5use std::mem;
   6use std::ops::Range;
   7use std::rc::Rc;
   8use std::sync::Arc;
   9use std::time::Duration;
  10
  11use gpui::{
  12    AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,
  13    FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, KeyContext,
  14    Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Render, Stateful,
  15    StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun, TextStyle,
  16    TextStyleRefinement, actions, point, quad,
  17};
  18use language::{Language, LanguageRegistry, Rope};
  19use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};
  20use pulldown_cmark::Alignment;
  21use theme::SyntaxTheme;
  22use ui::{Tooltip, prelude::*};
  23use util::{ResultExt, TryFutureExt};
  24
  25use crate::parser::CodeBlockKind;
  26
  27/// A callback function that can be used to customize the style of links based on the destination URL.
  28/// If the callback returns `None`, the default link style will be used.
  29type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
  30
  31#[derive(Clone)]
  32pub struct MarkdownStyle {
  33    pub base_text_style: TextStyle,
  34    pub code_block: StyleRefinement,
  35    pub code_block_overflow_x_scroll: bool,
  36    pub inline_code: TextStyleRefinement,
  37    pub block_quote: TextStyleRefinement,
  38    pub link: TextStyleRefinement,
  39    pub link_callback: Option<LinkStyleCallback>,
  40    pub rule_color: Hsla,
  41    pub block_quote_border_color: Hsla,
  42    pub syntax: Arc<SyntaxTheme>,
  43    pub selection_background_color: Hsla,
  44    pub heading: StyleRefinement,
  45    pub table_overflow_x_scroll: bool,
  46}
  47
  48impl Default for MarkdownStyle {
  49    fn default() -> Self {
  50        Self {
  51            base_text_style: Default::default(),
  52            code_block: Default::default(),
  53            code_block_overflow_x_scroll: false,
  54            inline_code: Default::default(),
  55            block_quote: Default::default(),
  56            link: Default::default(),
  57            link_callback: None,
  58            rule_color: Default::default(),
  59            block_quote_border_color: Default::default(),
  60            syntax: Arc::new(SyntaxTheme::default()),
  61            selection_background_color: Default::default(),
  62            heading: Default::default(),
  63            table_overflow_x_scroll: false,
  64        }
  65    }
  66}
  67
  68pub struct Markdown {
  69    source: SharedString,
  70    selection: Selection,
  71    pressed_link: Option<RenderedLink>,
  72    autoscroll_request: Option<usize>,
  73    style: MarkdownStyle,
  74    parsed_markdown: ParsedMarkdown,
  75    should_reparse: bool,
  76    pending_parse: Option<Task<Option<()>>>,
  77    focus_handle: FocusHandle,
  78    language_registry: Option<Arc<LanguageRegistry>>,
  79    fallback_code_block_language: Option<String>,
  80    open_url: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
  81    options: Options,
  82    copied_code_blocks: HashSet<ElementId>,
  83}
  84
  85#[derive(Debug)]
  86struct Options {
  87    parse_links_only: bool,
  88    copy_code_block_buttons: bool,
  89}
  90
  91actions!(markdown, [Copy]);
  92
  93impl Markdown {
  94    pub fn new(
  95        source: SharedString,
  96        style: MarkdownStyle,
  97        language_registry: Option<Arc<LanguageRegistry>>,
  98        fallback_code_block_language: Option<String>,
  99        cx: &mut Context<Self>,
 100    ) -> Self {
 101        let focus_handle = cx.focus_handle();
 102        let mut this = Self {
 103            source,
 104            selection: Selection::default(),
 105            pressed_link: None,
 106            autoscroll_request: None,
 107            style,
 108            should_reparse: false,
 109            parsed_markdown: ParsedMarkdown::default(),
 110            pending_parse: None,
 111            focus_handle,
 112            language_registry,
 113            fallback_code_block_language,
 114            options: Options {
 115                parse_links_only: false,
 116                copy_code_block_buttons: true,
 117            },
 118            open_url: None,
 119            copied_code_blocks: HashSet::new(),
 120        };
 121        this.parse(cx);
 122        this
 123    }
 124
 125    pub fn open_url(
 126        self,
 127        open_url: impl Fn(SharedString, &mut Window, &mut App) + 'static,
 128    ) -> Self {
 129        Self {
 130            open_url: Some(Box::new(open_url)),
 131            ..self
 132        }
 133    }
 134
 135    pub fn new_text(source: SharedString, style: MarkdownStyle, cx: &mut Context<Self>) -> Self {
 136        let focus_handle = cx.focus_handle();
 137        let mut this = Self {
 138            source,
 139            selection: Selection::default(),
 140            pressed_link: None,
 141            autoscroll_request: None,
 142            style,
 143            should_reparse: false,
 144            parsed_markdown: ParsedMarkdown::default(),
 145            pending_parse: None,
 146            focus_handle,
 147            language_registry: None,
 148            fallback_code_block_language: None,
 149            options: Options {
 150                parse_links_only: true,
 151                copy_code_block_buttons: true,
 152            },
 153            open_url: None,
 154            copied_code_blocks: HashSet::new(),
 155        };
 156        this.parse(cx);
 157        this
 158    }
 159
 160    pub fn source(&self) -> &str {
 161        &self.source
 162    }
 163
 164    pub fn append(&mut self, text: &str, cx: &mut Context<Self>) {
 165        self.source = SharedString::new(self.source.to_string() + text);
 166        self.parse(cx);
 167    }
 168
 169    pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) {
 170        if source == self.source() {
 171            return;
 172        }
 173        self.source = source;
 174        self.selection = Selection::default();
 175        self.autoscroll_request = None;
 176        self.pending_parse = None;
 177        self.should_reparse = false;
 178        self.parsed_markdown = ParsedMarkdown::default();
 179        self.parse(cx);
 180    }
 181
 182    pub fn parsed_markdown(&self) -> &ParsedMarkdown {
 183        &self.parsed_markdown
 184    }
 185
 186    fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
 187        if self.selection.end <= self.selection.start {
 188            return;
 189        }
 190        let text = text.text_for_range(self.selection.start..self.selection.end);
 191        cx.write_to_clipboard(ClipboardItem::new_string(text));
 192    }
 193
 194    fn parse(&mut self, cx: &mut Context<Self>) {
 195        if self.source.is_empty() {
 196            return;
 197        }
 198
 199        if self.pending_parse.is_some() {
 200            self.should_reparse = true;
 201            return;
 202        }
 203
 204        let source = self.source.clone();
 205        let parse_text_only = self.options.parse_links_only;
 206        let language_registry = self.language_registry.clone();
 207        let fallback = self.fallback_code_block_language.clone();
 208        let parsed = cx.background_spawn(async move {
 209            if parse_text_only {
 210                return anyhow::Ok(ParsedMarkdown {
 211                    events: Arc::from(parse_links_only(source.as_ref())),
 212                    source,
 213                    languages: HashMap::default(),
 214                });
 215            }
 216            let (events, language_names) = parse_markdown(&source);
 217            let mut languages = HashMap::with_capacity(language_names.len());
 218            for name in language_names {
 219                if let Some(registry) = language_registry.as_ref() {
 220                    let language = if !name.is_empty() {
 221                        registry.language_for_name(&name)
 222                    } else if let Some(fallback) = &fallback {
 223                        registry.language_for_name(fallback)
 224                    } else {
 225                        continue;
 226                    };
 227                    if let Ok(language) = language.await {
 228                        languages.insert(name, language);
 229                    }
 230                }
 231            }
 232            anyhow::Ok(ParsedMarkdown {
 233                source,
 234                events: Arc::from(events),
 235                languages,
 236            })
 237        });
 238
 239        self.should_reparse = false;
 240        self.pending_parse = Some(cx.spawn(async move |this, cx| {
 241            async move {
 242                let parsed = parsed.await?;
 243                this.update(cx, |this, cx| {
 244                    this.parsed_markdown = parsed;
 245                    this.pending_parse.take();
 246                    if this.should_reparse {
 247                        this.parse(cx);
 248                    }
 249                    cx.notify();
 250                })
 251                .ok();
 252                anyhow::Ok(())
 253            }
 254            .log_err()
 255            .await
 256        }));
 257    }
 258
 259    pub fn copy_code_block_buttons(mut self, should_copy: bool) -> Self {
 260        self.options.copy_code_block_buttons = should_copy;
 261        self
 262    }
 263}
 264
 265impl Render for Markdown {
 266    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 267        MarkdownElement::new(cx.entity().clone(), self.style.clone())
 268    }
 269}
 270
 271impl Focusable for Markdown {
 272    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 273        self.focus_handle.clone()
 274    }
 275}
 276
 277#[derive(Copy, Clone, Default, Debug)]
 278struct Selection {
 279    start: usize,
 280    end: usize,
 281    reversed: bool,
 282    pending: bool,
 283}
 284
 285impl Selection {
 286    fn set_head(&mut self, head: usize) {
 287        if head < self.tail() {
 288            if !self.reversed {
 289                self.end = self.start;
 290                self.reversed = true;
 291            }
 292            self.start = head;
 293        } else {
 294            if self.reversed {
 295                self.start = self.end;
 296                self.reversed = false;
 297            }
 298            self.end = head;
 299        }
 300    }
 301
 302    fn tail(&self) -> usize {
 303        if self.reversed { self.end } else { self.start }
 304    }
 305}
 306
 307#[derive(Default)]
 308pub struct ParsedMarkdown {
 309    source: SharedString,
 310    events: Arc<[(Range<usize>, MarkdownEvent)]>,
 311    languages: HashMap<SharedString, Arc<Language>>,
 312}
 313
 314impl ParsedMarkdown {
 315    pub fn source(&self) -> &SharedString {
 316        &self.source
 317    }
 318
 319    pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
 320        &self.events
 321    }
 322}
 323
 324pub struct MarkdownElement {
 325    markdown: Entity<Markdown>,
 326    style: MarkdownStyle,
 327}
 328
 329impl MarkdownElement {
 330    fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
 331        Self { markdown, style }
 332    }
 333
 334    fn paint_selection(
 335        &self,
 336        bounds: Bounds<Pixels>,
 337        rendered_text: &RenderedText,
 338        window: &mut Window,
 339        cx: &mut App,
 340    ) {
 341        let selection = self.markdown.read(cx).selection;
 342        let selection_start = rendered_text.position_for_source_index(selection.start);
 343        let selection_end = rendered_text.position_for_source_index(selection.end);
 344
 345        if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
 346            selection_start.zip(selection_end)
 347        {
 348            if start_position.y == end_position.y {
 349                window.paint_quad(quad(
 350                    Bounds::from_corners(
 351                        start_position,
 352                        point(end_position.x, end_position.y + end_line_height),
 353                    ),
 354                    Pixels::ZERO,
 355                    self.style.selection_background_color,
 356                    Edges::default(),
 357                    Hsla::transparent_black(),
 358                    BorderStyle::default(),
 359                ));
 360            } else {
 361                window.paint_quad(quad(
 362                    Bounds::from_corners(
 363                        start_position,
 364                        point(bounds.right(), start_position.y + start_line_height),
 365                    ),
 366                    Pixels::ZERO,
 367                    self.style.selection_background_color,
 368                    Edges::default(),
 369                    Hsla::transparent_black(),
 370                    BorderStyle::default(),
 371                ));
 372
 373                if end_position.y > start_position.y + start_line_height {
 374                    window.paint_quad(quad(
 375                        Bounds::from_corners(
 376                            point(bounds.left(), start_position.y + start_line_height),
 377                            point(bounds.right(), end_position.y),
 378                        ),
 379                        Pixels::ZERO,
 380                        self.style.selection_background_color,
 381                        Edges::default(),
 382                        Hsla::transparent_black(),
 383                        BorderStyle::default(),
 384                    ));
 385                }
 386
 387                window.paint_quad(quad(
 388                    Bounds::from_corners(
 389                        point(bounds.left(), end_position.y),
 390                        point(end_position.x, end_position.y + end_line_height),
 391                    ),
 392                    Pixels::ZERO,
 393                    self.style.selection_background_color,
 394                    Edges::default(),
 395                    Hsla::transparent_black(),
 396                    BorderStyle::default(),
 397                ));
 398            }
 399        }
 400    }
 401
 402    fn paint_mouse_listeners(
 403        &self,
 404        hitbox: &Hitbox,
 405        rendered_text: &RenderedText,
 406        window: &mut Window,
 407        cx: &mut App,
 408    ) {
 409        let is_hovering_link = hitbox.is_hovered(window)
 410            && !self.markdown.read(cx).selection.pending
 411            && rendered_text
 412                .link_for_position(window.mouse_position())
 413                .is_some();
 414
 415        if is_hovering_link {
 416            window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox));
 417        } else {
 418            window.set_cursor_style(CursorStyle::IBeam, Some(hitbox));
 419        }
 420
 421        self.on_mouse_event(window, cx, {
 422            let rendered_text = rendered_text.clone();
 423            let hitbox = hitbox.clone();
 424            move |markdown, event: &MouseDownEvent, phase, window, cx| {
 425                if hitbox.is_hovered(window) {
 426                    if phase.bubble() {
 427                        if let Some(link) = rendered_text.link_for_position(event.position) {
 428                            markdown.pressed_link = Some(link.clone());
 429                        } else {
 430                            let source_index =
 431                                match rendered_text.source_index_for_position(event.position) {
 432                                    Ok(ix) | Err(ix) => ix,
 433                                };
 434                            let range = if event.click_count == 2 {
 435                                rendered_text.surrounding_word_range(source_index)
 436                            } else if event.click_count == 3 {
 437                                rendered_text.surrounding_line_range(source_index)
 438                            } else {
 439                                source_index..source_index
 440                            };
 441                            markdown.selection = Selection {
 442                                start: range.start,
 443                                end: range.end,
 444                                reversed: false,
 445                                pending: true,
 446                            };
 447                            window.focus(&markdown.focus_handle);
 448                            window.prevent_default();
 449                        }
 450
 451                        cx.notify();
 452                    }
 453                } else if phase.capture() {
 454                    markdown.selection = Selection::default();
 455                    markdown.pressed_link = None;
 456                    cx.notify();
 457                }
 458            }
 459        });
 460        self.on_mouse_event(window, cx, {
 461            let rendered_text = rendered_text.clone();
 462            let hitbox = hitbox.clone();
 463            let was_hovering_link = is_hovering_link;
 464            move |markdown, event: &MouseMoveEvent, phase, window, cx| {
 465                if phase.capture() {
 466                    return;
 467                }
 468
 469                if markdown.selection.pending {
 470                    let source_index = match rendered_text.source_index_for_position(event.position)
 471                    {
 472                        Ok(ix) | Err(ix) => ix,
 473                    };
 474                    markdown.selection.set_head(source_index);
 475                    markdown.autoscroll_request = Some(source_index);
 476                    cx.notify();
 477                } else {
 478                    let is_hovering_link = hitbox.is_hovered(window)
 479                        && rendered_text.link_for_position(event.position).is_some();
 480                    if is_hovering_link != was_hovering_link {
 481                        cx.notify();
 482                    }
 483                }
 484            }
 485        });
 486        self.on_mouse_event(window, cx, {
 487            let rendered_text = rendered_text.clone();
 488            move |markdown, event: &MouseUpEvent, phase, window, cx| {
 489                if phase.bubble() {
 490                    if let Some(pressed_link) = markdown.pressed_link.take() {
 491                        if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
 492                            if let Some(open_url) = markdown.open_url.as_mut() {
 493                                open_url(pressed_link.destination_url, window, cx);
 494                            } else {
 495                                cx.open_url(&pressed_link.destination_url);
 496                            }
 497                        }
 498                    }
 499                } else if markdown.selection.pending {
 500                    markdown.selection.pending = false;
 501                    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
 502                    {
 503                        let text = rendered_text
 504                            .text_for_range(markdown.selection.start..markdown.selection.end);
 505                        cx.write_to_primary(ClipboardItem::new_string(text))
 506                    }
 507                    cx.notify();
 508                }
 509            }
 510        });
 511    }
 512
 513    fn autoscroll(
 514        &self,
 515        rendered_text: &RenderedText,
 516        window: &mut Window,
 517        cx: &mut App,
 518    ) -> Option<()> {
 519        let autoscroll_index = self
 520            .markdown
 521            .update(cx, |markdown, _| markdown.autoscroll_request.take())?;
 522        let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
 523
 524        let text_style = self.style.base_text_style.clone();
 525        let font_id = window.text_system().resolve_font(&text_style.font());
 526        let font_size = text_style.font_size.to_pixels(window.rem_size());
 527        let em_width = window.text_system().em_width(font_id, font_size).unwrap();
 528        window.request_autoscroll(Bounds::from_corners(
 529            point(position.x - 3. * em_width, position.y - 3. * line_height),
 530            point(position.x + 3. * em_width, position.y + 3. * line_height),
 531        ));
 532        Some(())
 533    }
 534
 535    fn on_mouse_event<T: MouseEvent>(
 536        &self,
 537        window: &mut Window,
 538        _cx: &mut App,
 539        mut f: impl 'static
 540        + FnMut(&mut Markdown, &T, DispatchPhase, &mut Window, &mut Context<Markdown>),
 541    ) {
 542        window.on_mouse_event({
 543            let markdown = self.markdown.downgrade();
 544            move |event, phase, window, cx| {
 545                markdown
 546                    .update(cx, |markdown, cx| f(markdown, event, phase, window, cx))
 547                    .log_err();
 548            }
 549        });
 550    }
 551}
 552
 553impl Element for MarkdownElement {
 554    type RequestLayoutState = RenderedMarkdown;
 555    type PrepaintState = Hitbox;
 556
 557    fn id(&self) -> Option<ElementId> {
 558        None
 559    }
 560
 561    fn request_layout(
 562        &mut self,
 563        _id: Option<&GlobalElementId>,
 564        window: &mut Window,
 565        cx: &mut App,
 566    ) -> (gpui::LayoutId, Self::RequestLayoutState) {
 567        let mut builder = MarkdownElementBuilder::new(
 568            self.style.base_text_style.clone(),
 569            self.style.syntax.clone(),
 570        );
 571        let parsed_markdown = &self.markdown.read(cx).parsed_markdown;
 572        let markdown_end = if let Some(last) = parsed_markdown.events.last() {
 573            last.0.end
 574        } else {
 575            0
 576        };
 577        for (range, event) in parsed_markdown.events.iter() {
 578            match event {
 579                MarkdownEvent::Start(tag) => {
 580                    match tag {
 581                        MarkdownTag::Paragraph => {
 582                            builder.push_div(
 583                                div().mb_2().line_height(rems(1.3)),
 584                                range,
 585                                markdown_end,
 586                            );
 587                        }
 588                        MarkdownTag::Heading { level, .. } => {
 589                            let mut heading = div().mb_2();
 590                            heading = match level {
 591                                pulldown_cmark::HeadingLevel::H1 => heading.text_3xl(),
 592                                pulldown_cmark::HeadingLevel::H2 => heading.text_2xl(),
 593                                pulldown_cmark::HeadingLevel::H3 => heading.text_xl(),
 594                                pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
 595                                _ => heading,
 596                            };
 597                            heading.style().refine(&self.style.heading);
 598                            builder.push_text_style(
 599                                self.style.heading.text_style().clone().unwrap_or_default(),
 600                            );
 601                            builder.push_div(heading, range, markdown_end);
 602                        }
 603                        MarkdownTag::BlockQuote => {
 604                            builder.push_text_style(self.style.block_quote.clone());
 605                            builder.push_div(
 606                                div()
 607                                    .pl_4()
 608                                    .mb_2()
 609                                    .border_l_4()
 610                                    .border_color(self.style.block_quote_border_color),
 611                                range,
 612                                markdown_end,
 613                            );
 614                        }
 615                        MarkdownTag::CodeBlock(kind) => {
 616                            let language = if let CodeBlockKind::Fenced(language) = kind {
 617                                parsed_markdown.languages.get(language).cloned()
 618                            } else {
 619                                None
 620                            };
 621
 622                            // This is a parent container that we can position the copy button inside.
 623                            builder.push_div(div().relative().w_full(), range, markdown_end);
 624
 625                            let mut code_block = div()
 626                                .id(("code-block", range.start))
 627                                .rounded_lg()
 628                                .map(|mut code_block| {
 629                                    if self.style.code_block_overflow_x_scroll {
 630                                        code_block.style().restrict_scroll_to_axis = Some(true);
 631                                        code_block.flex().overflow_x_scroll()
 632                                    } else {
 633                                        code_block.w_full()
 634                                    }
 635                                });
 636                            code_block.style().refine(&self.style.code_block);
 637                            if let Some(code_block_text_style) = &self.style.code_block.text {
 638                                builder.push_text_style(code_block_text_style.to_owned());
 639                            }
 640                            builder.push_code_block(language);
 641                            builder.push_div(code_block, range, markdown_end);
 642                        }
 643                        MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
 644                        MarkdownTag::List(bullet_index) => {
 645                            builder.push_list(*bullet_index);
 646                            builder.push_div(div().pl_4(), range, markdown_end);
 647                        }
 648                        MarkdownTag::Item => {
 649                            let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
 650                                format!("{}.", bullet_index)
 651                            } else {
 652                                "".to_string()
 653                            };
 654                            builder.push_div(
 655                                div()
 656                                    .mb_1()
 657                                    .h_flex()
 658                                    .items_start()
 659                                    .gap_1()
 660                                    .line_height(rems(1.3))
 661                                    .child(bullet),
 662                                range,
 663                                markdown_end,
 664                            );
 665                            // Without `w_0`, text doesn't wrap to the width of the container.
 666                            builder.push_div(div().flex_1().w_0(), range, markdown_end);
 667                        }
 668                        MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
 669                            font_style: Some(FontStyle::Italic),
 670                            ..Default::default()
 671                        }),
 672                        MarkdownTag::Strong => builder.push_text_style(TextStyleRefinement {
 673                            font_weight: Some(FontWeight::BOLD),
 674                            ..Default::default()
 675                        }),
 676                        MarkdownTag::Strikethrough => {
 677                            builder.push_text_style(TextStyleRefinement {
 678                                strikethrough: Some(StrikethroughStyle {
 679                                    thickness: px(1.),
 680                                    color: None,
 681                                }),
 682                                ..Default::default()
 683                            })
 684                        }
 685                        MarkdownTag::Link { dest_url, .. } => {
 686                            if builder.code_block_stack.is_empty() {
 687                                builder.push_link(dest_url.clone(), range.clone());
 688                                let style = self
 689                                    .style
 690                                    .link_callback
 691                                    .as_ref()
 692                                    .and_then(|callback| callback(dest_url, cx))
 693                                    .unwrap_or_else(|| self.style.link.clone());
 694                                builder.push_text_style(style)
 695                            }
 696                        }
 697                        MarkdownTag::MetadataBlock(_) => {}
 698                        MarkdownTag::Table(alignments) => {
 699                            builder.table_alignments = alignments.clone();
 700                            builder.push_div(
 701                                div()
 702                                    .id(("table", range.start))
 703                                    .flex()
 704                                    .border_1()
 705                                    .border_color(cx.theme().colors().border)
 706                                    .rounded_sm()
 707                                    .when(self.style.table_overflow_x_scroll, |mut table| {
 708                                        table.style().restrict_scroll_to_axis = Some(true);
 709                                        table.overflow_x_scroll()
 710                                    }),
 711                                range,
 712                                markdown_end,
 713                            );
 714                            // This inner `v_flex` is so the table rows will stack vertically without disrupting the `overflow_x_scroll`.
 715                            builder.push_div(div().v_flex().flex_grow(), range, markdown_end);
 716                        }
 717                        MarkdownTag::TableHead => {
 718                            builder.push_div(
 719                                div()
 720                                    .flex()
 721                                    .justify_between()
 722                                    .border_b_1()
 723                                    .border_color(cx.theme().colors().border),
 724                                range,
 725                                markdown_end,
 726                            );
 727                            builder.push_text_style(TextStyleRefinement {
 728                                font_weight: Some(FontWeight::BOLD),
 729                                ..Default::default()
 730                            });
 731                        }
 732                        MarkdownTag::TableRow => {
 733                            builder.push_div(
 734                                div().h_flex().justify_between().px_1().py_0p5(),
 735                                range,
 736                                markdown_end,
 737                            );
 738                        }
 739                        MarkdownTag::TableCell => {
 740                            let column_count = builder.table_alignments.len();
 741
 742                            builder.push_div(
 743                                div()
 744                                    .flex()
 745                                    .px_1()
 746                                    .w(relative(1. / column_count as f32))
 747                                    .truncate(),
 748                                range,
 749                                markdown_end,
 750                            );
 751                        }
 752                        _ => log::debug!("unsupported markdown tag {:?}", tag),
 753                    }
 754                }
 755                MarkdownEvent::End(tag) => match tag {
 756                    MarkdownTagEnd::Paragraph => {
 757                        builder.pop_div();
 758                    }
 759                    MarkdownTagEnd::Heading(_) => {
 760                        builder.pop_div();
 761                        builder.pop_text_style()
 762                    }
 763                    MarkdownTagEnd::BlockQuote(_kind) => {
 764                        builder.pop_text_style();
 765                        builder.pop_div()
 766                    }
 767                    MarkdownTagEnd::CodeBlock => {
 768                        builder.trim_trailing_newline();
 769
 770                        builder.pop_div();
 771                        builder.pop_code_block();
 772                        if self.style.code_block.text.is_some() {
 773                            builder.pop_text_style();
 774                        }
 775
 776                        if self.markdown.read(cx).options.copy_code_block_buttons {
 777                            builder.flush_text();
 778                            builder.modify_current_div(|el| {
 779                                let id =
 780                                    ElementId::NamedInteger("copy-markdown-code".into(), range.end);
 781                                let was_copied =
 782                                    self.markdown.read(cx).copied_code_blocks.contains(&id);
 783                                let copy_button = div().absolute().top_1().right_1().w_5().child(
 784                                    IconButton::new(
 785                                        id.clone(),
 786                                        if was_copied {
 787                                            IconName::Check
 788                                        } else {
 789                                            IconName::Copy
 790                                        },
 791                                    )
 792                                    .icon_color(Color::Muted)
 793                                    .shape(ui::IconButtonShape::Square)
 794                                    .tooltip(Tooltip::text("Copy Code"))
 795                                    .on_click({
 796                                        let id = id.clone();
 797                                        let markdown = self.markdown.clone();
 798                                        let code = without_fences(
 799                                            parsed_markdown.source()[range.clone()].trim(),
 800                                        )
 801                                        .to_string();
 802                                        move |_event, _window, cx| {
 803                                            let id = id.clone();
 804                                            markdown.update(cx, |this, cx| {
 805                                                this.copied_code_blocks.insert(id.clone());
 806
 807                                                cx.write_to_clipboard(ClipboardItem::new_string(
 808                                                    code.clone(),
 809                                                ));
 810
 811                                                cx.spawn(async move |this, cx| {
 812                                                    cx.background_executor()
 813                                                        .timer(Duration::from_secs(2))
 814                                                        .await;
 815
 816                                                    cx.update(|cx| {
 817                                                        this.update(cx, |this, cx| {
 818                                                            this.copied_code_blocks.remove(&id);
 819                                                            cx.notify();
 820                                                        })
 821                                                    })
 822                                                    .ok();
 823                                                })
 824                                                .detach();
 825                                            });
 826                                        }
 827                                    }),
 828                                );
 829
 830                                el.child(copy_button)
 831                            });
 832                        }
 833
 834                        // Pop the parent container.
 835                        builder.pop_div();
 836                    }
 837                    MarkdownTagEnd::HtmlBlock => builder.pop_div(),
 838                    MarkdownTagEnd::List(_) => {
 839                        builder.pop_list();
 840                        builder.pop_div();
 841                    }
 842                    MarkdownTagEnd::Item => {
 843                        builder.pop_div();
 844                        builder.pop_div();
 845                    }
 846                    MarkdownTagEnd::Emphasis => builder.pop_text_style(),
 847                    MarkdownTagEnd::Strong => builder.pop_text_style(),
 848                    MarkdownTagEnd::Strikethrough => builder.pop_text_style(),
 849                    MarkdownTagEnd::Link => {
 850                        if builder.code_block_stack.is_empty() {
 851                            builder.pop_text_style()
 852                        }
 853                    }
 854                    MarkdownTagEnd::Table => {
 855                        builder.pop_div();
 856                        builder.pop_div();
 857                        builder.table_alignments.clear();
 858                    }
 859                    MarkdownTagEnd::TableHead => {
 860                        builder.pop_div();
 861                        builder.pop_text_style();
 862                    }
 863                    MarkdownTagEnd::TableRow => {
 864                        builder.pop_div();
 865                    }
 866                    MarkdownTagEnd::TableCell => {
 867                        builder.pop_div();
 868                    }
 869                    _ => log::debug!("unsupported markdown tag end: {:?}", tag),
 870                },
 871                MarkdownEvent::Text => {
 872                    builder.push_text(&parsed_markdown.source[range.clone()], range.start);
 873                }
 874                MarkdownEvent::SubstitutedText(text) => {
 875                    builder.push_text(text, range.start);
 876                }
 877                MarkdownEvent::Code => {
 878                    builder.push_text_style(self.style.inline_code.clone());
 879                    builder.push_text(&parsed_markdown.source[range.clone()], range.start);
 880                    builder.pop_text_style();
 881                }
 882                MarkdownEvent::Html => {
 883                    builder.push_text(&parsed_markdown.source[range.clone()], range.start);
 884                }
 885                MarkdownEvent::InlineHtml => {
 886                    builder.push_text(&parsed_markdown.source[range.clone()], range.start);
 887                }
 888                MarkdownEvent::Rule => {
 889                    builder.push_div(
 890                        div()
 891                            .border_b_1()
 892                            .my_2()
 893                            .border_color(self.style.rule_color),
 894                        range,
 895                        markdown_end,
 896                    );
 897                    builder.pop_div()
 898                }
 899                MarkdownEvent::SoftBreak => builder.push_text(" ", range.start),
 900                MarkdownEvent::HardBreak => builder.push_text("\n", range.start),
 901                _ => log::error!("unsupported markdown event {:?}", event),
 902            }
 903        }
 904        let mut rendered_markdown = builder.build();
 905        let child_layout_id = rendered_markdown.element.request_layout(window, cx);
 906        let layout_id = window.request_layout(gpui::Style::default(), [child_layout_id], cx);
 907        (layout_id, rendered_markdown)
 908    }
 909
 910    fn prepaint(
 911        &mut self,
 912        _id: Option<&GlobalElementId>,
 913        bounds: Bounds<Pixels>,
 914        rendered_markdown: &mut Self::RequestLayoutState,
 915        window: &mut Window,
 916        cx: &mut App,
 917    ) -> Self::PrepaintState {
 918        let focus_handle = self.markdown.read(cx).focus_handle.clone();
 919        window.set_focus_handle(&focus_handle, cx);
 920
 921        let hitbox = window.insert_hitbox(bounds, false);
 922        rendered_markdown.element.prepaint(window, cx);
 923        self.autoscroll(&rendered_markdown.text, window, cx);
 924        hitbox
 925    }
 926
 927    fn paint(
 928        &mut self,
 929        _id: Option<&GlobalElementId>,
 930        bounds: Bounds<Pixels>,
 931        rendered_markdown: &mut Self::RequestLayoutState,
 932        hitbox: &mut Self::PrepaintState,
 933        window: &mut Window,
 934        cx: &mut App,
 935    ) {
 936        let mut context = KeyContext::default();
 937        context.add("Markdown");
 938        window.set_key_context(context);
 939        let entity = self.markdown.clone();
 940        window.on_action(std::any::TypeId::of::<crate::Copy>(), {
 941            let text = rendered_markdown.text.clone();
 942            move |_, phase, window, cx| {
 943                let text = text.clone();
 944                if phase == DispatchPhase::Bubble {
 945                    entity.update(cx, move |this, cx| this.copy(&text, window, cx))
 946                }
 947            }
 948        });
 949
 950        self.paint_mouse_listeners(hitbox, &rendered_markdown.text, window, cx);
 951        rendered_markdown.element.paint(window, cx);
 952        self.paint_selection(bounds, &rendered_markdown.text, window, cx);
 953    }
 954}
 955
 956impl IntoElement for MarkdownElement {
 957    type Element = Self;
 958
 959    fn into_element(self) -> Self::Element {
 960        self
 961    }
 962}
 963
 964enum AnyDiv {
 965    Div(Div),
 966    Stateful(Stateful<Div>),
 967}
 968
 969impl AnyDiv {
 970    fn into_any_element(self) -> AnyElement {
 971        match self {
 972            Self::Div(div) => div.into_any_element(),
 973            Self::Stateful(div) => div.into_any_element(),
 974        }
 975    }
 976}
 977
 978impl From<Div> for AnyDiv {
 979    fn from(value: Div) -> Self {
 980        Self::Div(value)
 981    }
 982}
 983
 984impl From<Stateful<Div>> for AnyDiv {
 985    fn from(value: Stateful<Div>) -> Self {
 986        Self::Stateful(value)
 987    }
 988}
 989
 990impl Styled for AnyDiv {
 991    fn style(&mut self) -> &mut StyleRefinement {
 992        match self {
 993            Self::Div(div) => div.style(),
 994            Self::Stateful(div) => div.style(),
 995        }
 996    }
 997}
 998
 999impl ParentElement for AnyDiv {
1000    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1001        match self {
1002            Self::Div(div) => div.extend(elements),
1003            Self::Stateful(div) => div.extend(elements),
1004        }
1005    }
1006}
1007
1008struct MarkdownElementBuilder {
1009    div_stack: Vec<AnyDiv>,
1010    rendered_lines: Vec<RenderedLine>,
1011    pending_line: PendingLine,
1012    rendered_links: Vec<RenderedLink>,
1013    current_source_index: usize,
1014    base_text_style: TextStyle,
1015    text_style_stack: Vec<TextStyleRefinement>,
1016    code_block_stack: Vec<Option<Arc<Language>>>,
1017    list_stack: Vec<ListStackEntry>,
1018    table_alignments: Vec<Alignment>,
1019    syntax_theme: Arc<SyntaxTheme>,
1020}
1021
1022#[derive(Default)]
1023struct PendingLine {
1024    text: String,
1025    runs: Vec<TextRun>,
1026    source_mappings: Vec<SourceMapping>,
1027}
1028
1029struct ListStackEntry {
1030    bullet_index: Option<u64>,
1031}
1032
1033impl MarkdownElementBuilder {
1034    fn new(base_text_style: TextStyle, syntax_theme: Arc<SyntaxTheme>) -> Self {
1035        Self {
1036            div_stack: vec![div().debug_selector(|| "inner".into()).into()],
1037            rendered_lines: Vec::new(),
1038            pending_line: PendingLine::default(),
1039            rendered_links: Vec::new(),
1040            current_source_index: 0,
1041            base_text_style,
1042            text_style_stack: Vec::new(),
1043            code_block_stack: Vec::new(),
1044            list_stack: Vec::new(),
1045            table_alignments: Vec::new(),
1046            syntax_theme,
1047        }
1048    }
1049
1050    fn push_text_style(&mut self, style: TextStyleRefinement) {
1051        self.text_style_stack.push(style);
1052    }
1053
1054    fn text_style(&self) -> TextStyle {
1055        let mut style = self.base_text_style.clone();
1056        for refinement in &self.text_style_stack {
1057            style.refine(refinement);
1058        }
1059        style
1060    }
1061
1062    fn pop_text_style(&mut self) {
1063        self.text_style_stack.pop();
1064    }
1065
1066    fn push_div(&mut self, div: impl Into<AnyDiv>, range: &Range<usize>, markdown_end: usize) {
1067        let mut div = div.into();
1068        self.flush_text();
1069
1070        if range.start == 0 {
1071            // Remove the top margin on the first element.
1072            div.style().refine(&StyleRefinement {
1073                margin: gpui::EdgesRefinement {
1074                    top: Some(Length::Definite(px(0.).into())),
1075                    left: None,
1076                    right: None,
1077                    bottom: None,
1078                },
1079                ..Default::default()
1080            });
1081        }
1082
1083        if range.end == markdown_end {
1084            div.style().refine(&StyleRefinement {
1085                margin: gpui::EdgesRefinement {
1086                    top: None,
1087                    left: None,
1088                    right: None,
1089                    bottom: Some(Length::Definite(rems(0.).into())),
1090                },
1091                ..Default::default()
1092            });
1093        }
1094
1095        self.div_stack.push(div);
1096    }
1097
1098    fn modify_current_div(&mut self, f: impl FnOnce(AnyDiv) -> AnyDiv) {
1099        self.flush_text();
1100        if let Some(div) = self.div_stack.pop() {
1101            self.div_stack.push(f(div));
1102        }
1103    }
1104
1105    fn pop_div(&mut self) {
1106        self.flush_text();
1107        let div = self.div_stack.pop().unwrap().into_any_element();
1108        self.div_stack.last_mut().unwrap().extend(iter::once(div));
1109    }
1110
1111    fn push_list(&mut self, bullet_index: Option<u64>) {
1112        self.list_stack.push(ListStackEntry { bullet_index });
1113    }
1114
1115    fn next_bullet_index(&mut self) -> Option<u64> {
1116        self.list_stack.last_mut().and_then(|entry| {
1117            let item_index = entry.bullet_index.as_mut()?;
1118            *item_index += 1;
1119            Some(*item_index - 1)
1120        })
1121    }
1122
1123    fn pop_list(&mut self) {
1124        self.list_stack.pop();
1125    }
1126
1127    fn push_code_block(&mut self, language: Option<Arc<Language>>) {
1128        self.code_block_stack.push(language);
1129    }
1130
1131    fn pop_code_block(&mut self) {
1132        self.code_block_stack.pop();
1133    }
1134
1135    fn push_link(&mut self, destination_url: SharedString, source_range: Range<usize>) {
1136        self.rendered_links.push(RenderedLink {
1137            source_range,
1138            destination_url,
1139        });
1140    }
1141
1142    fn push_text(&mut self, text: &str, source_index: usize) {
1143        self.pending_line.source_mappings.push(SourceMapping {
1144            rendered_index: self.pending_line.text.len(),
1145            source_index,
1146        });
1147        self.pending_line.text.push_str(text);
1148        self.current_source_index = source_index + text.len();
1149
1150        if let Some(Some(language)) = self.code_block_stack.last() {
1151            let mut offset = 0;
1152            for (range, highlight_id) in language.highlight_text(&Rope::from(text), 0..text.len()) {
1153                if range.start > offset {
1154                    self.pending_line
1155                        .runs
1156                        .push(self.text_style().to_run(range.start - offset));
1157                }
1158
1159                let mut run_style = self.text_style();
1160                if let Some(highlight) = highlight_id.style(&self.syntax_theme) {
1161                    run_style = run_style.highlight(highlight);
1162                }
1163                self.pending_line.runs.push(run_style.to_run(range.len()));
1164                offset = range.end;
1165            }
1166
1167            if offset < text.len() {
1168                self.pending_line
1169                    .runs
1170                    .push(self.text_style().to_run(text.len() - offset));
1171            }
1172        } else {
1173            self.pending_line
1174                .runs
1175                .push(self.text_style().to_run(text.len()));
1176        }
1177    }
1178
1179    fn trim_trailing_newline(&mut self) {
1180        if self.pending_line.text.ends_with('\n') {
1181            self.pending_line
1182                .text
1183                .truncate(self.pending_line.text.len() - 1);
1184            self.pending_line.runs.last_mut().unwrap().len -= 1;
1185            self.current_source_index -= 1;
1186        }
1187    }
1188
1189    fn flush_text(&mut self) {
1190        let line = mem::take(&mut self.pending_line);
1191        if line.text.is_empty() {
1192            return;
1193        }
1194
1195        let text = StyledText::new(line.text).with_runs(line.runs);
1196        self.rendered_lines.push(RenderedLine {
1197            layout: text.layout().clone(),
1198            source_mappings: line.source_mappings,
1199            source_end: self.current_source_index,
1200        });
1201        self.div_stack.last_mut().unwrap().extend([text.into_any()]);
1202    }
1203
1204    fn build(mut self) -> RenderedMarkdown {
1205        debug_assert_eq!(self.div_stack.len(), 1);
1206        self.flush_text();
1207        RenderedMarkdown {
1208            element: self.div_stack.pop().unwrap().into_any_element(),
1209            text: RenderedText {
1210                lines: self.rendered_lines.into(),
1211                links: self.rendered_links.into(),
1212            },
1213        }
1214    }
1215}
1216
1217struct RenderedLine {
1218    layout: TextLayout,
1219    source_mappings: Vec<SourceMapping>,
1220    source_end: usize,
1221}
1222
1223impl RenderedLine {
1224    fn rendered_index_for_source_index(&self, source_index: usize) -> usize {
1225        let mapping = match self
1226            .source_mappings
1227            .binary_search_by_key(&source_index, |probe| probe.source_index)
1228        {
1229            Ok(ix) => &self.source_mappings[ix],
1230            Err(ix) => &self.source_mappings[ix - 1],
1231        };
1232        mapping.rendered_index + (source_index - mapping.source_index)
1233    }
1234
1235    fn source_index_for_rendered_index(&self, rendered_index: usize) -> usize {
1236        let mapping = match self
1237            .source_mappings
1238            .binary_search_by_key(&rendered_index, |probe| probe.rendered_index)
1239        {
1240            Ok(ix) => &self.source_mappings[ix],
1241            Err(ix) => &self.source_mappings[ix - 1],
1242        };
1243        mapping.source_index + (rendered_index - mapping.rendered_index)
1244    }
1245
1246    fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1247        let line_rendered_index;
1248        let out_of_bounds;
1249        match self.layout.index_for_position(position) {
1250            Ok(ix) => {
1251                line_rendered_index = ix;
1252                out_of_bounds = false;
1253            }
1254            Err(ix) => {
1255                line_rendered_index = ix;
1256                out_of_bounds = true;
1257            }
1258        };
1259        let source_index = self.source_index_for_rendered_index(line_rendered_index);
1260        if out_of_bounds {
1261            Err(source_index)
1262        } else {
1263            Ok(source_index)
1264        }
1265    }
1266}
1267
1268#[derive(Copy, Clone, Debug, Default)]
1269struct SourceMapping {
1270    rendered_index: usize,
1271    source_index: usize,
1272}
1273
1274pub struct RenderedMarkdown {
1275    element: AnyElement,
1276    text: RenderedText,
1277}
1278
1279#[derive(Clone)]
1280struct RenderedText {
1281    lines: Rc<[RenderedLine]>,
1282    links: Rc<[RenderedLink]>,
1283}
1284
1285#[derive(Clone, Eq, PartialEq)]
1286struct RenderedLink {
1287    source_range: Range<usize>,
1288    destination_url: SharedString,
1289}
1290
1291impl RenderedText {
1292    fn source_index_for_position(&self, position: Point<Pixels>) -> Result<usize, usize> {
1293        let mut lines = self.lines.iter().peekable();
1294
1295        while let Some(line) = lines.next() {
1296            let line_bounds = line.layout.bounds();
1297            if position.y > line_bounds.bottom() {
1298                if let Some(next_line) = lines.peek() {
1299                    if position.y < next_line.layout.bounds().top() {
1300                        return Err(line.source_end);
1301                    }
1302                }
1303
1304                continue;
1305            }
1306
1307            return line.source_index_for_position(position);
1308        }
1309
1310        Err(self.lines.last().map_or(0, |line| line.source_end))
1311    }
1312
1313    fn position_for_source_index(&self, source_index: usize) -> Option<(Point<Pixels>, Pixels)> {
1314        for line in self.lines.iter() {
1315            let line_source_start = line.source_mappings.first().unwrap().source_index;
1316            if source_index < line_source_start {
1317                break;
1318            } else if source_index > line.source_end {
1319                continue;
1320            } else {
1321                let line_height = line.layout.line_height();
1322                let rendered_index_within_line = line.rendered_index_for_source_index(source_index);
1323                let position = line.layout.position_for_index(rendered_index_within_line)?;
1324                return Some((position, line_height));
1325            }
1326        }
1327        None
1328    }
1329
1330    fn surrounding_word_range(&self, source_index: usize) -> Range<usize> {
1331        for line in self.lines.iter() {
1332            if source_index > line.source_end {
1333                continue;
1334            }
1335
1336            let line_rendered_start = line.source_mappings.first().unwrap().rendered_index;
1337            let rendered_index_in_line =
1338                line.rendered_index_for_source_index(source_index) - line_rendered_start;
1339            let text = line.layout.text();
1340            let previous_space = if let Some(idx) = text[0..rendered_index_in_line].rfind(' ') {
1341                idx + ' '.len_utf8()
1342            } else {
1343                0
1344            };
1345            let next_space = if let Some(idx) = text[rendered_index_in_line..].find(' ') {
1346                rendered_index_in_line + idx
1347            } else {
1348                text.len()
1349            };
1350
1351            return line.source_index_for_rendered_index(line_rendered_start + previous_space)
1352                ..line.source_index_for_rendered_index(line_rendered_start + next_space);
1353        }
1354
1355        source_index..source_index
1356    }
1357
1358    fn surrounding_line_range(&self, source_index: usize) -> Range<usize> {
1359        for line in self.lines.iter() {
1360            if source_index > line.source_end {
1361                continue;
1362            }
1363            let line_source_start = line.source_mappings.first().unwrap().source_index;
1364            return line_source_start..line.source_end;
1365        }
1366
1367        source_index..source_index
1368    }
1369
1370    fn text_for_range(&self, range: Range<usize>) -> String {
1371        let mut ret = vec![];
1372
1373        for line in self.lines.iter() {
1374            if range.start > line.source_end {
1375                continue;
1376            }
1377            let line_source_start = line.source_mappings.first().unwrap().source_index;
1378            if range.end < line_source_start {
1379                break;
1380            }
1381
1382            let text = line.layout.text();
1383
1384            let start = if range.start < line_source_start {
1385                0
1386            } else {
1387                line.rendered_index_for_source_index(range.start)
1388            };
1389            let end = if range.end > line.source_end {
1390                line.rendered_index_for_source_index(line.source_end)
1391            } else {
1392                line.rendered_index_for_source_index(range.end)
1393            }
1394            .min(text.len());
1395
1396            ret.push(text[start..end].to_string());
1397        }
1398        ret.join("\n")
1399    }
1400
1401    fn link_for_position(&self, position: Point<Pixels>) -> Option<&RenderedLink> {
1402        let source_index = self.source_index_for_position(position).ok()?;
1403        self.links
1404            .iter()
1405            .find(|link| link.source_range.contains(&source_index))
1406    }
1407}
1408
1409/// Some markdown blocks are indented, and others have e.g. ```rust … ``` around them.
1410/// If this block is fenced with backticks, strip them off (and the language name).
1411/// We use this when copying code blocks to the clipboard.
1412fn without_fences(mut markdown: &str) -> &str {
1413    if let Some(opening_backticks) = markdown.find("```") {
1414        markdown = &markdown[opening_backticks..];
1415
1416        // Trim off the next newline. This also trims off a language name if it's there.
1417        if let Some(newline) = markdown.find('\n') {
1418            markdown = &markdown[newline + 1..];
1419        }
1420    };
1421
1422    if let Some(closing_backticks) = markdown.rfind("```") {
1423        markdown = &markdown[..closing_backticks];
1424    };
1425
1426    markdown
1427}
1428
1429#[cfg(test)]
1430mod tests {
1431    use super::*;
1432
1433    #[test]
1434    fn test_without_fences() {
1435        let input = "```rust\nlet x = 5;\n```";
1436        assert_eq!(without_fences(input), "let x = 5;\n");
1437
1438        let input = "   ```\nno language\n```   ";
1439        assert_eq!(without_fences(input), "no language\n");
1440
1441        let input = "plain text";
1442        assert_eq!(without_fences(input), "plain text");
1443
1444        let input = "```python\nprint('hello')\nprint('world')\n```";
1445        assert_eq!(without_fences(input), "print('hello')\nprint('world')\n");
1446    }
1447}