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.

<img width="1288" height="436" alt="image"
src="https://github.com/user-attachments/assets/b35b949a-8bc4-47db-82ef-ed835e9ac06f"
/>

Release Notes:

- Added support for rendering Markdown frontmatter metadata blocks in
Markdown Preview and Agent Panel.
This commit is contained in:
Smit Barmase 2026-05-28 19:17:58 +05:30 committed by GitHub
parent 92b0efeee0
commit f0ed342c19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 523 additions and 27 deletions

View file

@ -775,6 +775,7 @@ impl ContentBlock {
None,
MarkdownOptions {
render_mermaid_diagrams: true,
render_metadata_blocks: true,
..Default::default()
},
cx,

View file

@ -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<str>, Arc<Language>>,
pub root_block_starts: Arc<[usize]>,
pub(crate) html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>,
pub(crate) metadata_blocks: BTreeMap<usize, ParsedMetadataBlock>,
pub(crate) mermaid_diagrams: BTreeMap<usize, ParsedMarkdownMermaidDiagram>,
pub heading_slugs: HashMap<SharedString, usize>,
pub footnote_definitions: HashMap<SharedString, usize>,
@ -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<usize>,
block_range: &Range<usize>,
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<Range<usize>> = 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) = &current_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<TextAlign> {
}
}
struct MetadataCellStyle {
row_index: usize,
is_key: bool,
}
struct MarkdownElementBuilder {
div_stack: Vec<AnyDiv>,
rendered_lines: Vec<RenderedLine>,
@ -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<SharedString> + '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<String> = 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()

View file

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

View file

@ -37,10 +37,23 @@ pub(crate) struct ParsedMarkdownData {
pub language_paths: HashSet<Arc<str>>,
pub root_block_starts: Vec<usize>,
pub html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>,
pub metadata_blocks: BTreeMap<usize, ParsedMetadataBlock>,
pub heading_slugs: HashMap<SharedString, usize>,
pub footnote_definitions: HashMap<SharedString, usize>,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ParsedMetadataBlock {
pub content_range: Range<usize>,
pub rows: Option<Vec<MetadataRow>>,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct MetadataRow {
pub key: Range<usize>,
pub value: Range<usize>,
}
impl ParseState {
fn push_event(&mut self, range: Range<usize>, event: MarkdownEvent) {
match &event {
@ -149,27 +162,83 @@ fn build_heading_slugs(
slugs
}
fn parse_metadata_table_rows(source: &str, source_range: Range<usize>) -> Option<Vec<MetadataRow>> {
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<usize>) -> Range<usize> {
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<Range<usize>> = 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::<Vec<_>>();
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(" <!--\nrdoc-file=string.c\n-->\nReturns", false, false),
parse_markdown_with_options(
" <!--\nrdoc-file=string.c\n-->\nReturns",
false,
false,
false
),
ParsedMarkdownData {
events: vec![
(2..30, RootStart),
@ -851,6 +1110,7 @@ mod tests {
"&nbsp;&nbsp; https://some.url some \\`&#9658;\\` 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&#46;com!",
false,
false,
false,
)
.events,
vec![
@ -1253,6 +1568,7 @@ mod tests {
"Visit https://example.com/cat\\/é&#8205;☕ 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

View file

@ -223,6 +223,7 @@ impl MarkdownPreviewView {
parse_html: true,
render_mermaid_diagrams: true,
parse_heading_slugs: true,
render_metadata_blocks: true,
..Default::default()
},
cx,