From f0ed342c1956b874b67e0f5cd2e42a9cd409ac32 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 28 May 2026 19:17:58 +0530 Subject: [PATCH] markdown: Add frontmatter metadata block rendering (#57845) Adds opt-in rendering for Markdown frontmatter metadata blocks in Markdown Preview and agent markdown. - Simple `key: value` metadata blocks now render as a two-column table, while more complex metadata falls back to a code-style block. - Metadata block content and key/value rows are parsed in the parser step, and the request layout simply takes over rendering. image Release Notes: - Added support for rendering Markdown frontmatter metadata blocks in Markdown Preview and Agent Panel. --- crates/acp_thread/src/acp_thread.rs | 1 + crates/markdown/src/markdown.rs | 188 +++++++++- crates/markdown/src/mermaid.rs | 6 +- crates/markdown/src/parser.rs | 354 +++++++++++++++++- .../src/markdown_preview_view.rs | 1 + 5 files changed, 523 insertions(+), 27 deletions(-) diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index e27f09da557..c42694cd8c6 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -775,6 +775,7 @@ impl ContentBlock { None, MarkdownOptions { render_mermaid_diagrams: true, + render_metadata_blocks: true, ..Default::default() }, cx, diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index 4cbcbe85678..7fcbf393fb4 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -40,7 +40,8 @@ use gpui::{ use language::{CharClassifier, Language, LanguageRegistry, Rope}; use parser::CodeBlockMetadata; use parser::{ - MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown_with_options, + MarkdownEvent, MarkdownTag, MarkdownTagEnd, ParsedMetadataBlock, parse_links_only, + parse_markdown_with_options, }; use pulldown_cmark::{Alignment, BlockQuoteKind}; use sum_tree::TreeMap; @@ -350,6 +351,7 @@ pub struct MarkdownOptions { pub parse_html: bool, pub render_mermaid_diagrams: bool, pub parse_heading_slugs: bool, + pub render_metadata_blocks: bool, } #[derive(Clone, Copy, PartialEq, Eq)] @@ -847,6 +849,7 @@ impl Markdown { let should_parse_html = self.options.parse_html; let should_render_mermaid_diagrams = self.options.render_mermaid_diagrams; let should_parse_heading_slugs = self.options.parse_heading_slugs; + let should_parse_metadata_blocks = self.options.render_metadata_blocks; let language_registry = self.language_registry.clone(); let fallback = self.fallback_code_block_language.clone(); @@ -860,6 +863,7 @@ impl Markdown { languages_by_path: TreeMap::default(), root_block_starts: Arc::default(), html_blocks: BTreeMap::default(), + metadata_blocks: BTreeMap::default(), mermaid_diagrams: BTreeMap::default(), heading_slugs: HashMap::default(), footnote_definitions: HashMap::default(), @@ -868,13 +872,18 @@ impl Markdown { ); } - let parsed = - parse_markdown_with_options(&source, should_parse_html, should_parse_heading_slugs); + let parsed = parse_markdown_with_options( + &source, + should_parse_html, + should_parse_heading_slugs, + should_parse_metadata_blocks, + ); let events = parsed.events; let language_names = parsed.language_names; let paths = parsed.language_paths; let root_block_starts = parsed.root_block_starts; let html_blocks = parsed.html_blocks; + let metadata_blocks = parsed.metadata_blocks; let heading_slugs = parsed.heading_slugs; let footnote_definitions = parsed.footnote_definitions; let mermaid_diagrams = if should_render_mermaid_diagrams { @@ -942,6 +951,7 @@ impl Markdown { languages_by_path, root_block_starts: Arc::from(root_block_starts), html_blocks, + metadata_blocks, mermaid_diagrams, heading_slugs, footnote_definitions, @@ -1070,6 +1080,7 @@ pub struct ParsedMarkdown { pub languages_by_path: TreeMap, Arc>, pub root_block_starts: Arc<[usize]>, pub(crate) html_blocks: BTreeMap, + pub(crate) metadata_blocks: BTreeMap, pub(crate) mermaid_diagrams: BTreeMap, pub heading_slugs: HashMap, pub footnote_definitions: HashMap, @@ -1398,6 +1409,114 @@ impl MarkdownElement { builder.pop_text_style(); } + fn push_metadata_block( + &self, + builder: &mut MarkdownElementBuilder, + source: &str, + metadata_block: &ParsedMetadataBlock, + markdown_end: usize, + cx: &App, + ) { + let content_range = &metadata_block.content_range; + if let Some(rows) = metadata_block.rows.as_deref() { + builder.push_div( + div() + .grid() + .grid_cols(2) + .w_full() + .mb_2() + .border_1() + .border_color(cx.theme().colors().border) + .rounded_sm() + .overflow_hidden(), + content_range, + markdown_end, + ); + + for (row_index, row) in rows.iter().enumerate() { + self.push_metadata_cell( + builder, + source, + row.key.clone(), + content_range, + markdown_end, + MetadataCellStyle { + row_index, + is_key: true, + }, + cx, + ); + self.push_metadata_cell( + builder, + source, + row.value.clone(), + content_range, + markdown_end, + MetadataCellStyle { + row_index, + is_key: false, + }, + cx, + ); + } + + builder.pop_div(); + } else { + let mut metadata_block = div().w_full().rounded_md(); + metadata_block.style().refine(&self.style.code_block); + builder.push_text_style(self.style.code_block.text.to_owned()); + builder.push_code_block(None); + builder.push_div(metadata_block, content_range, markdown_end); + builder.push_text(&source[content_range.clone()], content_range.clone()); + builder.trim_trailing_newline(); + builder.pop_div(); + builder.pop_code_block(); + builder.pop_text_style(); + } + } + + fn push_metadata_cell( + &self, + builder: &mut MarkdownElementBuilder, + source: &str, + text_range: Range, + block_range: &Range, + markdown_end: usize, + cell_style: MetadataCellStyle, + cx: &App, + ) { + builder.push_div( + div() + .flex() + .flex_col() + .min_w_0() + .px_2() + .py_1() + .border_color(cx.theme().colors().border) + .when(cell_style.row_index > 0, |this| this.border_t_1()) + .when(!cell_style.is_key, |this| this.border_l_1()) + .when(cell_style.is_key, |this| { + this.bg(cx.theme().colors().panel_background) + }), + block_range, + markdown_end, + ); + + let text_style = if cell_style.is_key { + TextStyleRefinement { + color: Some(cx.theme().colors().text_muted), + font_weight: Some(FontWeight::SEMIBOLD), + ..Default::default() + } + } else { + TextStyleRefinement::default() + }; + builder.push_text_style(text_style); + builder.push_text(&source[text_range.clone()], text_range); + builder.pop_text_style(); + builder.pop_div(); + } + fn push_markdown_list_item( &self, builder: &mut MarkdownElementBuilder, @@ -1809,6 +1928,7 @@ impl Element for MarkdownElement { let mut current_img_block_range: Option> = None; let mut handled_html_block = false; let mut rendered_mermaid_block = false; + let mut rendered_metadata_block = false; for (index, (range, event)) in parsed_markdown.events.iter().enumerate() { // Skip alt text for images that rendered if let Some(current_img_block_range) = ¤t_img_block_range @@ -1832,6 +1952,13 @@ impl Element for MarkdownElement { continue; } + if rendered_metadata_block { + if matches!(event, MarkdownEvent::End(MarkdownTagEnd::MetadataBlock(_))) { + rendered_metadata_block = false; + } + continue; + } + match event { MarkdownEvent::RootStart => { if self.show_root_block_markers { @@ -2147,7 +2274,20 @@ impl Element for MarkdownElement { ); builder.push_div(div().flex_1().w_0(), range, markdown_end); } - MarkdownTag::MetadataBlock(_) => {} + MarkdownTag::MetadataBlock(_) => { + if let Some(metadata_block) = + parsed_markdown.metadata_blocks.get(&range.start) + { + self.push_metadata_block( + &mut builder, + &parsed_markdown.source, + metadata_block, + markdown_end, + cx, + ); + rendered_metadata_block = true; + } + } MarkdownTag::Table(alignments) => { builder.table.start(alignments.clone()); @@ -2359,6 +2499,7 @@ impl Element for MarkdownElement { builder.pop_div(); builder.pop_div(); } + MarkdownTagEnd::MetadataBlock(_) => {} _ => log::debug!("unsupported markdown tag end: {:?}", tag), }, MarkdownEvent::Text => { @@ -2752,6 +2893,11 @@ fn alignment_to_text_align(alignment: Alignment) -> Option { } } +struct MetadataCellStyle { + row_index: usize, + is_key: bool, +} + struct MarkdownElementBuilder { div_stack: Vec, rendered_lines: Vec, @@ -3586,6 +3732,34 @@ mod tests { render_markdown_with_language_registry(markdown, None, cx) } + #[gpui::test] + fn test_frontmatter_renders_without_delimiters(cx: &mut TestAppContext) { + let rendered = render_markdown_with_options( + "---\ntitle: Post\n---\nBody", + None, + MarkdownOptions { + render_metadata_blocks: true, + ..Default::default() + }, + cx, + ); + assert_eq!(rendered.text_for_range(0..24), "title\nPost\nBody"); + } + + #[gpui::test] + fn test_frontmatter_falls_back_to_code_block_for_nested_yaml(cx: &mut TestAppContext) { + let rendered = render_markdown_with_options( + "---\ntags:\n - zed\n---\nBody", + None, + MarkdownOptions { + render_metadata_blocks: true, + ..Default::default() + }, + cx, + ); + assert_eq!(rendered.text_for_range(0..26), "tags:\n - zed\nBody"); + } + fn render_markdown_with_code_span_link( markdown: &str, callback: impl Fn(&str, &App) -> Option + 'static, @@ -3873,7 +4047,7 @@ mod tests { #[test] fn test_table_checkbox_detection() { let md = "| Done |\n|------|\n| [x] |\n| [ ] |"; - let events = crate::parser::parse_markdown_with_options(md, false, false).events; + let events = crate::parser::parse_markdown_with_options(md, false, false, false).events; let mut in_table = false; let mut cell_texts: Vec = Vec::new(); @@ -3915,7 +4089,7 @@ mod tests { #[test] fn test_table_checkbox_marker_source_range() { let md = "| Done |\n|------|\n| [x] |\n| [ ] |"; - let events = crate::parser::parse_markdown_with_options(md, false, false).events; + let events = crate::parser::parse_markdown_with_options(md, false, false, false).events; let mut in_cell = false; let mut pending_text = String::new(); @@ -4192,7 +4366,7 @@ mod tests { } fn has_code_block(markdown: &str) -> bool { - let parsed_data = parse_markdown_with_options(markdown, false, false); + let parsed_data = parse_markdown_with_options(markdown, false, false, false); parsed_data .events .iter() diff --git a/crates/markdown/src/mermaid.rs b/crates/markdown/src/mermaid.rs index 8730c318f0c..4acceb2577b 100644 --- a/crates/markdown/src/mermaid.rs +++ b/crates/markdown/src/mermaid.rs @@ -686,7 +686,8 @@ mod tests { #[test] fn test_extract_mermaid_diagrams_parses_scale() { let markdown = "```mermaid 150\ngraph TD;\n```\n\n```rust\nfn main() {}\n```"; - let events = crate::parser::parse_markdown_with_options(markdown, false, false).events; + let events = + crate::parser::parse_markdown_with_options(markdown, false, false, false).events; let diagrams = extract_mermaid_diagrams(markdown, &events); assert_eq!(diagrams.len(), 1); @@ -702,7 +703,8 @@ mod tests { "```mermaid\nblock-beta\n```\n\n", "```mermaid\nflowchart TD\n A --> B\n```", ); - let events = crate::parser::parse_markdown_with_options(markdown, false, false).events; + let events = + crate::parser::parse_markdown_with_options(markdown, false, false, false).events; let diagrams = extract_mermaid_diagrams(markdown, &events); assert_eq!( diagrams.len(), diff --git a/crates/markdown/src/parser.rs b/crates/markdown/src/parser.rs index 4e9d6c29830..6301d759f69 100644 --- a/crates/markdown/src/parser.rs +++ b/crates/markdown/src/parser.rs @@ -37,10 +37,23 @@ pub(crate) struct ParsedMarkdownData { pub language_paths: HashSet>, pub root_block_starts: Vec, pub html_blocks: BTreeMap, + pub metadata_blocks: BTreeMap, pub heading_slugs: HashMap, pub footnote_definitions: HashMap, } +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct ParsedMetadataBlock { + pub content_range: Range, + pub rows: Option>, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct MetadataRow { + pub key: Range, + pub value: Range, +} + impl ParseState { fn push_event(&mut self, range: Range, event: MarkdownEvent) { match &event { @@ -149,27 +162,83 @@ fn build_heading_slugs( slugs } +fn parse_metadata_table_rows(source: &str, source_range: Range) -> Option> { + let mut rows = Vec::new(); + let mut line_start = source_range.start; + + for line in source[source_range].split_inclusive('\n') { + let line_end = line_start + line.len(); + let content_end = line_start + line.trim_end_matches(['\r', '\n']).len(); + let content_range = line_start..content_end; + let line_text = &source[content_range.clone()]; + + if line_text.is_empty() + || line_text + .chars() + .next() + .is_some_and(|character| character.is_whitespace()) + { + return None; + } + + let delimiter = line_text.find(':')?; + let key = trim_metadata_range(source, content_range.start..content_range.start + delimiter); + let value = trim_metadata_range( + source, + content_range.start + delimiter + 1..content_range.end, + ); + if key.is_empty() || value.is_empty() { + return None; + } + + rows.push(MetadataRow { key, value }); + line_start = line_end; + } + + if rows.is_empty() { None } else { Some(rows) } +} + +fn trim_metadata_range(source: &str, range: Range) -> Range { + let text = &source[range.clone()]; + let start_offset = text.len() - text.trim_start().len(); + let end_offset = text.trim_end().len(); + let start = range.start + start_offset; + let end = (range.start + end_offset).max(start); + start..end +} + pub(crate) fn parse_markdown_with_options( text: &str, parse_html: bool, parse_heading_slugs: bool, + parse_metadata_blocks: bool, ) -> ParsedMarkdownData { let mut state = ParseState::default(); let mut language_names = HashSet::default(); let mut language_paths = HashSet::default(); let mut html_blocks = BTreeMap::default(); + let mut metadata_blocks = BTreeMap::default(); let mut within_link = false; let mut within_code_block = false; let mut within_metadata = false; - let mut parser = Parser::new_ext(text, PARSE_OPTIONS) + let mut current_metadata_block_start = None; + let mut metadata_block_content_range: Option> = None; + let parse_options = if parse_metadata_blocks { + PARSE_OPTIONS.union(Options::ENABLE_YAML_STYLE_METADATA_BLOCKS) + } else { + PARSE_OPTIONS + }; + let mut parser = Parser::new_ext(text, parse_options) .into_offset_iter() .peekable(); while let Some((pulldown_event, range)) = parser.next() { - if within_metadata { - if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::MetadataBlock { .. }) = + if within_metadata && !parse_metadata_blocks { + if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::MetadataBlock(_)) = pulldown_event { within_metadata = false; + current_metadata_block_start = None; + metadata_block_content_range = None; } continue; } @@ -216,9 +285,14 @@ pub(crate) fn parse_markdown_with_options( id: SharedString::from(id.into_string()), } } - pulldown_cmark::Tag::MetadataBlock(_kind) => { + pulldown_cmark::Tag::MetadataBlock(kind) => { within_metadata = true; - continue; + current_metadata_block_start = Some(range.start); + metadata_block_content_range = None; + if !parse_metadata_blocks { + continue; + } + MarkdownTag::MetadataBlock(kind) } pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Indented) => { within_code_block = true; @@ -347,6 +421,25 @@ pub(crate) fn parse_markdown_with_options( within_link = false; } else if let pulldown_cmark::TagEnd::CodeBlock = tag { within_code_block = false; + } else if let pulldown_cmark::TagEnd::MetadataBlock(_) = tag { + within_metadata = false; + let block_start = current_metadata_block_start.take(); + let content_range = metadata_block_content_range.take(); + if parse_metadata_blocks + && let (Some(block_start), Some(content_range)) = + (block_start, content_range) + { + metadata_blocks.insert( + block_start, + ParsedMetadataBlock { + rows: parse_metadata_table_rows(text, content_range.clone()), + content_range, + }, + ); + } + if !parse_metadata_blocks { + continue; + } } state.push_event(range, MarkdownEvent::End(tag)); } @@ -363,6 +456,18 @@ pub(crate) fn parse_markdown_with_options( } } + if within_metadata { + match &mut metadata_block_content_range { + Some(content_range) => { + content_range.start = content_range.start.min(range.start); + content_range.end = content_range.end.max(range.end); + } + None => metadata_block_content_range = Some(range.clone()), + } + state.push_event(range, MarkdownEvent::Text); + continue; + } + if within_code_block { let (range, event) = event_for(text, range, &parsed); state.push_event(range, event); @@ -541,6 +646,7 @@ pub(crate) fn parse_markdown_with_options( language_paths, root_block_starts: state.root_block_starts, html_blocks, + metadata_blocks, heading_slugs, footnote_definitions, } @@ -798,8 +904,8 @@ mod tests { use super::MarkdownTag::*; use super::*; - const UNWANTED_OPTIONS: Options = Options::ENABLE_YAML_STYLE_METADATA_BLOCKS - .union(Options::ENABLE_MATH) + const CONDITIONAL_OPTIONS: Options = Options::ENABLE_YAML_STYLE_METADATA_BLOCKS; + const UNWANTED_OPTIONS: Options = Options::ENABLE_MATH .union(Options::ENABLE_DEFINITION_LIST) .union(Options::ENABLE_WIKILINKS); @@ -807,21 +913,174 @@ mod tests { fn all_options_considered() { // The purpose of this is to fail when new options are added to pulldown_cmark, so that they // can be evaluated for inclusion. - assert_eq!(PARSE_OPTIONS.union(UNWANTED_OPTIONS), Options::all()); + assert_eq!( + PARSE_OPTIONS + .union(CONDITIONAL_OPTIONS) + .union(UNWANTED_OPTIONS), + Options::all() + ); } #[test] fn wanted_and_unwanted_options_disjoint() { assert_eq!( - PARSE_OPTIONS.intersection(UNWANTED_OPTIONS), + PARSE_OPTIONS + .union(CONDITIONAL_OPTIONS) + .intersection(UNWANTED_OPTIONS), Options::empty() ); } + #[test] + fn test_yaml_style_metadata_block() { + assert_eq!( + parse_markdown_with_options("---\ntitle: Post\n---\n# Heading", false, false, true), + ParsedMarkdownData { + events: vec![ + (0..19, RootStart), + (0..19, Start(MetadataBlock(MetadataBlockKind::YamlStyle))), + (4..16, Text), + ( + 0..19, + End(MarkdownTagEnd::MetadataBlock(MetadataBlockKind::YamlStyle)) + ), + (0..19, RootEnd(0)), + (20..29, RootStart), + ( + 20..29, + Start(Heading { + level: HeadingLevel::H1, + id: None, + classes: Vec::new(), + attrs: Vec::new(), + }) + ), + (22..29, Text), + (20..29, End(MarkdownTagEnd::Heading(HeadingLevel::H1))), + (20..29, RootEnd(1)), + ], + root_block_starts: vec![0, 20], + metadata_blocks: BTreeMap::from_iter([( + 0, + ParsedMetadataBlock { + content_range: 4..16, + rows: Some(vec![MetadataRow { + key: 4..9, + value: 11..15, + }]), + }, + )]), + ..Default::default() + } + ) + } + + #[test] + fn test_metadata_block_text_is_verbatim() { + let parsed = + parse_markdown_with_options("---\nurl: https://zed.dev\n---\nBody", false, false, true); + assert!( + parsed + .events + .iter() + .all(|(_, event)| !matches!(event, Start(Link { .. }))) + ); + } + + #[test] + fn test_metadata_blocks_store_table_rows() { + let parsed = parse_markdown_with_options( + "---\ntitle: Post\nauthor: Zed\n---\nBody", + false, + false, + true, + ); + + assert_eq!( + parsed.metadata_blocks, + BTreeMap::from_iter([( + 0, + ParsedMetadataBlock { + content_range: 4..28, + rows: Some(vec![ + MetadataRow { + key: 4..9, + value: 11..15, + }, + MetadataRow { + key: 16..22, + value: 24..27, + }, + ]), + }, + )]) + ); + } + + #[test] + fn test_metadata_blocks_store_fallback_for_nested_yaml() { + let parsed = + parse_markdown_with_options("---\ntags:\n - zed\n---\nBody", false, false, true); + + assert_eq!( + parsed.metadata_blocks, + BTreeMap::from_iter([( + 0, + ParsedMetadataBlock { + content_range: 4..18, + rows: None, + }, + )]) + ); + } + + #[test] + fn test_metadata_table_rows_parse_simple_colon_pairs() { + let source = "title: Post\nauthor: Zed\n"; + let Some(rows) = parse_metadata_table_rows(source, 0..source.len()) else { + panic!("expected metadata rows"); + }; + let pairs = rows + .into_iter() + .map(|row| (&source[row.key], &source[row.value])) + .collect::>(); + + assert_eq!(pairs, vec![("title", "Post"), ("author", "Zed")]); + } + + #[test] + fn test_metadata_table_rows_reject_non_simple_colon_pairs() { + for source in [ + "tags:\n - zed\n", + "title = Post\n", + "title:\n", + "title: \n", + ": Post\n", + " title: Post\n", + "\n", + ] { + assert!(parse_metadata_table_rows(source, 0..source.len()).is_none()); + } + } + + #[test] + fn test_trim_metadata_range_returns_valid_empty_range() { + let source = "key: \n"; + let trimmed = trim_metadata_range(source, 4..7); + + assert_eq!(trimmed, 7..7); + assert!(source[trimmed].is_empty()); + } + #[test] fn test_html_comments() { assert_eq!( - parse_markdown_with_options(" \nReturns", false, false), + parse_markdown_with_options( + " \nReturns", + false, + false, + false + ), ParsedMarkdownData { events: vec![ (2..30, RootStart), @@ -851,6 +1110,7 @@ mod tests { "   https://some.url some \\`►\\` text", false, false, + false, ), ParsedMarkdownData { events: vec![ @@ -891,6 +1151,7 @@ mod tests { "You can use the [GitHub Search API](https://docs.github.com/en", false, false, + false, ) .events, vec![ @@ -925,6 +1186,7 @@ mod tests { "-- --- ... \"double quoted\" 'single quoted' ----------", false, false, + false, ), ParsedMarkdownData { events: vec![ @@ -957,7 +1219,12 @@ mod tests { #[test] fn test_code_block_metadata() { assert_eq!( - parse_markdown_with_options("```rust\nfn main() {\n let a = 1;\n}\n```", false, false), + parse_markdown_with_options( + "```rust\nfn main() {\n let a = 1;\n}\n```", + false, + false, + false + ), ParsedMarkdownData { events: vec![ (0..37, RootStart), @@ -986,7 +1253,7 @@ mod tests { } ); assert_eq!( - parse_markdown_with_options(" fn main() {}", false, false), + parse_markdown_with_options(" fn main() {}", false, false, false), ParsedMarkdownData { events: vec![ (4..16, RootStart), @@ -1012,7 +1279,7 @@ mod tests { } fn assert_code_block_does_not_emit_links(markdown: &str) { - let parsed = parse_markdown_with_options(markdown, false, false); + let parsed = parse_markdown_with_options(markdown, false, false, false); let mut code_block_depth = 0; let mut code_block_count = 0; let mut saw_text_inside_code_block = false; @@ -1064,9 +1331,54 @@ mod tests { } #[test] - fn test_metadata_blocks_do_not_affect_root_blocks() { + fn test_metadata_blocks_are_root_blocks() { assert_eq!( - parse_markdown_with_options("+++\ntitle = \"Example\"\n+++\n\nParagraph", false, false), + parse_markdown_with_options( + "+++\ntitle = \"Example\"\n+++\n\nParagraph", + false, + false, + true + ), + ParsedMarkdownData { + events: vec![ + (0..25, RootStart), + (0..25, Start(MetadataBlock(MetadataBlockKind::PlusesStyle))), + (4..22, Text), + ( + 0..25, + End(MarkdownTagEnd::MetadataBlock( + MetadataBlockKind::PlusesStyle + )) + ), + (0..25, RootEnd(0)), + (27..36, RootStart), + (27..36, Start(Paragraph)), + (27..36, Text), + (27..36, End(MarkdownTagEnd::Paragraph)), + (27..36, RootEnd(1)), + ], + root_block_starts: vec![0, 27], + metadata_blocks: BTreeMap::from_iter([( + 0, + ParsedMetadataBlock { + content_range: 4..22, + rows: None, + }, + )]), + ..Default::default() + } + ); + } + + #[test] + fn test_metadata_blocks_are_omitted_by_default() { + assert_eq!( + parse_markdown_with_options( + "+++\ntitle = \"Example\"\n+++\n\nParagraph", + false, + false, + false + ), ParsedMarkdownData { events: vec![ (27..36, RootStart), @@ -1088,7 +1400,7 @@ mod tests { |------|---------| | [x] | Fix bug | | [ ] | Add feature |"; - let parsed = parse_markdown_with_options(markdown, false, false); + let parsed = parse_markdown_with_options(markdown, false, false, false); let mut in_table = false; let mut saw_task_list_marker = false; @@ -1164,6 +1476,7 @@ mod tests { "Text with a footnote[^1] and some more text.\n\n[^1]: This is the footnote content.", false, false, + false, ); assert_eq!( parsed.events, @@ -1194,6 +1507,7 @@ mod tests { "Text[^a] and[^b].\n\n[^a]: First.\n\n[^b]: Second.", false, false, + false, ); assert_eq!(parsed.footnote_definitions.len(), 2); assert!(parsed.footnote_definitions.contains_key("a")); @@ -1211,6 +1525,7 @@ mod tests { "https:/\\/example.com is equivalent to https://example.com!", false, false, + false, ) .events, vec![ @@ -1253,6 +1568,7 @@ mod tests { "Visit https://example.com/cat\\/é‍☕ for coffee!", false, false, + false, ) .events, [ @@ -1286,6 +1602,7 @@ mod tests { "# Hello World\n\n## Code `block`\n\n### Third Level\n\n#### Fourth Level\n\n## Hello World", false, true, + false, ); assert_eq!(parsed.heading_slugs.len(), 5); assert!(parsed.heading_slugs.contains_key("hello-world")); @@ -1301,6 +1618,7 @@ mod tests { "# Duplicate\n\nText\n\n## Duplicate\n\nMore text", false, true, + false, ); let first = parsed.heading_slugs.get("duplicate").copied(); let second = parsed.heading_slugs.get("duplicate-1").copied(); @@ -1311,7 +1629,7 @@ mod tests { #[test] fn test_heading_slug_collision_with_dedup_suffix() { - let parsed = parse_markdown_with_options("# Foo\n\n## Foo\n\n## Foo 1", false, true); + let parsed = parse_markdown_with_options("# Foo\n\n## Foo\n\n## Foo 1", false, true, false); assert_eq!(parsed.heading_slugs.len(), 3); assert!(parsed.heading_slugs.contains_key("foo")); assert!(parsed.heading_slugs.contains_key("foo-1")); @@ -1323,7 +1641,7 @@ mod tests { use pulldown_cmark::BlockQuoteKind; let markdown = "\n> [!NOTE]\n> A note.\n\n> [!TIP]\n> A tip.\n\n> [!IMPORTANT]\n> Important.\n\n> [!WARNING]\n> A warning.\n\n> [!CAUTION]\n> A caution.\n\n> Plain quote.\n"; - let parsed = parse_markdown_with_options(markdown, false, false); + let parsed = parse_markdown_with_options(markdown, false, false, false); let block_quote_kinds: Vec<_> = parsed .events diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 38ce126badb..2db1e9b0a24 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -223,6 +223,7 @@ impl MarkdownPreviewView { parse_html: true, render_mermaid_diagrams: true, parse_heading_slugs: true, + render_metadata_blocks: true, ..Default::default() }, cx,