use crate::markdown_elements::*;use gpui::FontWeight;use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};use std::{ops::Range, path::PathBuf}; pub fn parse_markdown( markdown_input: &str, file_location_directory: Option<PathBuf>,) -> ParsedMarkdown { let options = Options::all(); let parser = Parser::new_ext(markdown_input, options); let parser = MarkdownParser::new(parser.into_offset_iter().collect(), file_location_directory); let renderer = parser.parse_document(); ParsedMarkdown { children: renderer.parsed, }} struct MarkdownParser<'a> { tokens: Vec<(Event<'a>, Range<usize>)>, /// The current index in the tokens array cursor: usize, /// The blocks that we have successfully parsed so far parsed: Vec<ParsedMarkdownElement>, file_location_directory: Option<PathBuf>,} impl<'a> MarkdownParser<'a> { fn new( tokens: Vec<(Event<'a>, Range<usize>)>, file_location_directory: Option<PathBuf>, ) -> Self { Self { tokens, file_location_directory, cursor: 0, parsed: vec![], } } fn eof(&self) -> bool { if self.tokens.is_empty() { return true; } self.cursor >= self.tokens.len() - 1 } fn peek(&self, steps: usize) -> Option<&(Event, Range<usize>)> { if self.eof() || (steps + self.cursor) >= self.tokens.len() { return self.tokens.last(); } return self.tokens.get(self.cursor + steps); } fn previous(&self) -> Option<&(Event, Range<usize>)> { if self.cursor == 0 || self.cursor > self.tokens.len() { return None; } return self.tokens.get(self.cursor - 1); } fn current(&self) -> Option<&(Event, Range<usize>)> { return self.peek(0); } fn is_text_like(event: &Event) -> bool { match event { Event::Text(_) // Represent an inline code block | Event::Code(_) | Event::Html(_) | Event::FootnoteReference(_) | Event::Start(Tag::Link { link_type: _, dest_url: _, title: _, id: _ }) | Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong) | Event::Start(Tag::Strikethrough) | Event::Start(Tag::Image { link_type: _, dest_url: _, title: _, id: _ }) => { return true; } _ => return false, } } fn parse_document(mut self) -> Self { while !self.eof() { if let Some(block) = self.parse_block() { self.parsed.push(block); } } self } fn parse_block(&mut self) -> Option<ParsedMarkdownElement> { let (current, source_range) = self.current().unwrap(); match current { Event::Start(tag) => match tag { Tag::Paragraph => { self.cursor += 1; let text = self.parse_text(false); Some(ParsedMarkdownElement::Paragraph(text)) } Tag::Heading { level, id: _, classes: _, attrs: _, } => { let level = *level; self.cursor += 1; let heading = self.parse_heading(level); Some(ParsedMarkdownElement::Heading(heading)) } Tag::Table(alignment) => { let alignment = alignment.clone(); self.cursor += 1; let table = self.parse_table(alignment); Some(ParsedMarkdownElement::Table(table)) } Tag::List(order) => { let order = *order; self.cursor += 1; let list = self.parse_list(1, order); Some(ParsedMarkdownElement::List(list)) } Tag::BlockQuote => { self.cursor += 1; let block_quote = self.parse_block_quote(); Some(ParsedMarkdownElement::BlockQuote(block_quote)) } Tag::CodeBlock(kind) => { let language = match kind { pulldown_cmark::CodeBlockKind::Indented => None, pulldown_cmark::CodeBlockKind::Fenced(language) => { if language.is_empty() { None } else { Some(language.to_string()) } } }; self.cursor += 1; let code_block = self.parse_code_block(language); Some(ParsedMarkdownElement::CodeBlock(code_block)) } _ => { self.cursor += 1; None } }, Event::Rule => { let source_range = source_range.clone(); self.cursor += 1; Some(ParsedMarkdownElement::HorizontalRule(source_range)) } _ => { self.cursor += 1; None } } } fn parse_text(&mut self, should_complete_on_soft_break: bool) -> ParsedMarkdownText { let (_current, source_range) = self.previous().unwrap(); let source_range = source_range.clone(); let mut text = String::new(); let mut bold_depth = 0; let mut italic_depth = 0; let mut strikethrough_depth = 0; let mut link: Option<Link> = None; let mut region_ranges: Vec<Range<usize>> = vec![]; let mut regions: Vec<ParsedRegion> = vec![]; let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![]; loop { if self.eof() { break; } let (current, _source_range) = self.current().unwrap(); let prev_len = text.len(); match current { Event::SoftBreak => { if should_complete_on_soft_break { break; } // `Some text\nSome more text` should be treated as a single line. text.push(' '); } Event::HardBreak => { break; } Event::Text(t) => { text.push_str(t.as_ref()); let mut style = MarkdownHighlightStyle::default(); if bold_depth > 0 { style.weight = FontWeight::BOLD; } if italic_depth > 0 { style.italic = true; } if strikethrough_depth > 0 { style.strikethrough = true; } if let Some(link) = link.clone() { region_ranges.push(prev_len..text.len()); regions.push(ParsedRegion { code: false, link: Some(link), }); style.underline = true; } if style != MarkdownHighlightStyle::default() { let mut new_highlight = true; if let Some((last_range, MarkdownHighlight::Style(last_style))) = highlights.last_mut() { if last_range.end == prev_len && last_style == &style { last_range.end = text.len(); new_highlight = false; } } if new_highlight { let range = prev_len..text.len(); highlights.push((range, MarkdownHighlight::Style(style))); } } } // Note: This event means "inline code" and not "code block" Event::Code(t) => { text.push_str(t.as_ref()); region_ranges.push(prev_len..text.len()); if link.is_some() { highlights.push(( prev_len..text.len(), MarkdownHighlight::Style(MarkdownHighlightStyle { underline: true, ..Default::default() }), )); } regions.push(ParsedRegion { code: true, link: link.clone(), }); } Event::Start(tag) => match tag { Tag::Emphasis => italic_depth += 1, Tag::Strong => bold_depth += 1, Tag::Strikethrough => strikethrough_depth += 1, Tag::Link { link_type: _, dest_url, title: _, id: _, } => { link = Link::identify( self.file_location_directory.clone(), dest_url.to_string(), ); } _ => { break; } }, Event::End(tag) => match tag { TagEnd::Emphasis => { italic_depth -= 1; } TagEnd::Strong => { bold_depth -= 1; } TagEnd::Strikethrough => { strikethrough_depth -= 1; } TagEnd::Link => { link = None; } TagEnd::Paragraph => { self.cursor += 1; break; } _ => { break; } }, _ => { break; } } self.cursor += 1; } ParsedMarkdownText { source_range, contents: text, highlights, regions, region_ranges, } } fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading { let (_event, source_range) = self.previous().unwrap(); let source_range = source_range.clone(); let text = self.parse_text(true); // Advance past the heading end tag self.cursor += 1; ParsedMarkdownHeading { source_range: source_range.clone(), level: match level { pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1, pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2, pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3, pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4, pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5, pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6, }, contents: text, } } fn parse_table(&mut self, alignment: Vec<Alignment>) -> ParsedMarkdownTable { let (_event, source_range) = self.previous().unwrap(); let source_range = source_range.clone(); let mut header = ParsedMarkdownTableRow::new(); let mut body = vec![]; let mut current_row = vec![]; let mut in_header = true; let column_alignments = alignment .iter() .map(|a| Self::convert_alignment(a)) .collect(); loop { if self.eof() { break; } let (current, _source_range) = self.current().unwrap(); match current { Event::Start(Tag::TableHead) | Event::Start(Tag::TableRow) | Event::End(TagEnd::TableCell) => { self.cursor += 1; } Event::Start(Tag::TableCell) => { self.cursor += 1; let cell_contents = self.parse_text(false); current_row.push(cell_contents); } Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => { self.cursor += 1; let new_row = std::mem::replace(&mut current_row, vec![]); if in_header { header.children = new_row; in_header = false; } else { let row = ParsedMarkdownTableRow::with_children(new_row); body.push(row); } } Event::End(TagEnd::Table) => { self.cursor += 1; break; } _ => { break; } } } ParsedMarkdownTable { source_range, header, body, column_alignments, } } fn convert_alignment(alignment: &Alignment) -> ParsedMarkdownTableAlignment { match alignment { Alignment::None => ParsedMarkdownTableAlignment::None, Alignment::Left => ParsedMarkdownTableAlignment::Left, Alignment::Center => ParsedMarkdownTableAlignment::Center, Alignment::Right => ParsedMarkdownTableAlignment::Right, } } fn parse_list(&mut self, depth: u16, order: Option<u64>) -> ParsedMarkdownList { let (_event, source_range) = self.previous().unwrap(); let source_range = source_range.clone(); let mut children = vec![]; let mut inside_list_item = false; let mut order = order; let mut task_item = None; let mut current_list_items: Vec<Box<ParsedMarkdownElement>> = vec![]; while !self.eof() { let (current, _source_range) = self.current().unwrap(); match current { Event::Start(Tag::List(order)) => { let order = *order; self.cursor += 1; let inner_list = self.parse_list(depth + 1, order); let block = ParsedMarkdownElement::List(inner_list); current_list_items.push(Box::new(block)); } Event::End(TagEnd::List(_)) => { self.cursor += 1; break; } Event::Start(Tag::Item) => { self.cursor += 1; inside_list_item = true; // Check for task list marker (`- [ ]` or `- [x]`) if let Some(next) = self.current() { match next.0 { Event::TaskListMarker(checked) => { task_item = Some(checked); self.cursor += 1; } _ => {} } } if let Some(next) = self.current() { // This is a plain list item. // For example `- some text` or `1. [Docs](./docs.md)` if MarkdownParser::is_text_like(&next.0) { let text = self.parse_text(false); let block = ParsedMarkdownElement::Paragraph(text); current_list_items.push(Box::new(block)); } else { let block = self.parse_block(); if let Some(block) = block { current_list_items.push(Box::new(block)); } } } } Event::End(TagEnd::Item) => { self.cursor += 1; let item_type = if let Some(checked) = task_item { ParsedMarkdownListItemType::Task(checked) } else if let Some(order) = order { ParsedMarkdownListItemType::Ordered(order) } else { ParsedMarkdownListItemType::Unordered }; if let Some(current) = order { order = Some(current + 1); } let contents = std::mem::replace(&mut current_list_items, vec![]); children.push(ParsedMarkdownListItem { contents, depth, item_type, }); inside_list_item = false; task_item = None; } _ => { if !inside_list_item { break; } let block = self.parse_block(); if let Some(block) = block { current_list_items.push(Box::new(block)); } }Lines 1–500 of 1171. Load more
use crate::markdown_elements::*; use gpui::FontWeight; use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd}; use std::{ops::Range, path::PathBuf}; pub fn parse_markdown( markdown_input: &str, file_location_directory: Option<PathBuf>, ) -> ParsedMarkdown { let options = Options::all(); let parser = Parser::new_ext(markdown_input, options); let parser = MarkdownParser::new(parser.into_offset_iter().collect(), file_location_directory); let renderer = parser.parse_document(); ParsedMarkdown { children: renderer.parsed, } } struct MarkdownParser<'a> { tokens: Vec<(Event<'a>, Range<usize>)>, /// The current index in the tokens array cursor: usize, /// The blocks that we have successfully parsed so far parsed: Vec<ParsedMarkdownElement>, file_location_directory: Option<PathBuf>, } impl<'a> MarkdownParser<'a> { fn new( tokens: Vec<(Event<'a>, Range<usize>)>, file_location_directory: Option<PathBuf>, ) -> Self { Self { tokens, file_location_directory, cursor: 0, parsed: vec![], } } fn eof(&self) -> bool { if self.tokens.is_empty() { return true; } self.cursor >= self.tokens.len() - 1 } fn peek(&self, steps: usize) -> Option<&(Event, Range<usize>)> { if self.eof() || (steps + self.cursor) >= self.tokens.len() { return self.tokens.last(); } return self.tokens.get(self.cursor + steps); } fn previous(&self) -> Option<&(Event, Range<usize>)> { if self.cursor == 0 || self.cursor > self.tokens.len() { return None; } return self.tokens.get(self.cursor - 1); } fn current(&self) -> Option<&(Event, Range<usize>)> { return self.peek(0); } fn is_text_like(event: &Event) -> bool { match event { Event::Text(_) // Represent an inline code block | Event::Code(_) | Event::Html(_) | Event::FootnoteReference(_) | Event::Start(Tag::Link { link_type: _, dest_url: _, title: _, id: _ }) | Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong) | Event::Start(Tag::Strikethrough) | Event::Start(Tag::Image { link_type: _, dest_url: _, title: _, id: _ }) => { return true; } _ => return false, } } fn parse_document(mut self) -> Self { while !self.eof() { if let Some(block) = self.parse_block() { self.parsed.push(block); } } self } fn parse_block(&mut self) -> Option<ParsedMarkdownElement> { let (current, source_range) = self.current().unwrap(); match current { Event::Start(tag) => match tag { Tag::Paragraph => { self.cursor += 1; let text = self.parse_text(false); Some(ParsedMarkdownElement::Paragraph(text)) } Tag::Heading { level, id: _, classes: _, attrs: _, } => { let level = *level; self.cursor += 1; let heading = self.parse_heading(level); Some(ParsedMarkdownElement::Heading(heading)) } Tag::Table(alignment) => { let alignment = alignment.clone(); self.cursor += 1; let table = self.parse_table(alignment); Some(ParsedMarkdownElement::Table(table)) } Tag::List(order) => { let order = *order; self.cursor += 1; let list = self.parse_list(1, order); Some(ParsedMarkdownElement::List(list)) } Tag::BlockQuote => { self.cursor += 1; let block_quote = self.parse_block_quote(); Some(ParsedMarkdownElement::BlockQuote(block_quote)) } Tag::CodeBlock(kind) => { let language = match kind { pulldown_cmark::CodeBlockKind::Indented => None, pulldown_cmark::CodeBlockKind::Fenced(language) => { if language.is_empty() { None } else { Some(language.to_string()) } } }; self.cursor += 1; let code_block = self.parse_code_block(language); Some(ParsedMarkdownElement::CodeBlock(code_block)) } _ => { self.cursor += 1; None } }, Event::Rule => { let source_range = source_range.clone(); self.cursor += 1; Some(ParsedMarkdownElement::HorizontalRule(source_range)) } _ => { self.cursor += 1; None } } } fn parse_text(&mut self, should_complete_on_soft_break: bool) -> ParsedMarkdownText { let (_current, source_range) = self.previous().unwrap(); let source_range = source_range.clone(); let mut text = String::new(); let mut bold_depth = 0; let mut italic_depth = 0; let mut strikethrough_depth = 0; let mut link: Option<Link> = None; let mut region_ranges: Vec<Range<usize>> = vec![]; let mut regions: Vec<ParsedRegion> = vec![]; let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![]; loop { if self.eof() { break; } let (current, _source_range) = self.current().unwrap(); let prev_len = text.len(); match current { Event::SoftBreak => { if should_complete_on_soft_break { break; } // `Some text\nSome more text` should be treated as a single line. text.push(' '); } Event::HardBreak => { break; } Event::Text(t) => { text.push_str(t.as_ref()); let mut style = MarkdownHighlightStyle::default(); if bold_depth > 0 { style.weight = FontWeight::BOLD; } if italic_depth > 0 { style.italic = true; } if strikethrough_depth > 0 { style.strikethrough = true; } if let Some(link) = link.clone() { region_ranges.push(prev_len..text.len()); regions.push(ParsedRegion { code: false, link: Some(link), }); style.underline = true; } if style != MarkdownHighlightStyle::default() { let mut new_highlight = true; if let Some((last_range, MarkdownHighlight::Style(last_style))) = highlights.last_mut() { if last_range.end == prev_len && last_style == &style { last_range.end = text.len(); new_highlight = false; } } if new_highlight { let range = prev_len..text.len(); highlights.push((range, MarkdownHighlight::Style(style))); } } } // Note: This event means "inline code" and not "code block" Event::Code(t) => { text.push_str(t.as_ref()); region_ranges.push(prev_len..text.len()); if link.is_some() { highlights.push(( prev_len..text.len(), MarkdownHighlight::Style(MarkdownHighlightStyle { underline: true, ..Default::default() }), )); } regions.push(ParsedRegion { code: true, link: link.clone(), }); } Event::Start(tag) => match tag { Tag::Emphasis => italic_depth += 1, Tag::Strong => bold_depth += 1, Tag::Strikethrough => strikethrough_depth += 1, Tag::Link { link_type: _, dest_url, title: _, id: _, } => { link = Link::identify( self.file_location_directory.clone(), dest_url.to_string(), ); } _ => { break; } }, Event::End(tag) => match tag { TagEnd::Emphasis => { italic_depth -= 1; } TagEnd::Strong => { bold_depth -= 1; } TagEnd::Strikethrough => { strikethrough_depth -= 1; } TagEnd::Link => { link = None; } TagEnd::Paragraph => { self.cursor += 1; break; } _ => { break; } }, _ => { break; } } self.cursor += 1; } ParsedMarkdownText { source_range, contents: text, highlights, regions, region_ranges, } } fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading { let (_event, source_range) = self.previous().unwrap(); let source_range = source_range.clone(); let text = self.parse_text(true); // Advance past the heading end tag self.cursor += 1; ParsedMarkdownHeading { source_range: source_range.clone(), level: match level { pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1, pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2, pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3, pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4, pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5, pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6, }, contents: text, } } fn parse_table(&mut self, alignment: Vec<Alignment>) -> ParsedMarkdownTable { let (_event, source_range) = self.previous().unwrap(); let source_range = source_range.clone(); let mut header = ParsedMarkdownTableRow::new(); let mut body = vec![]; let mut current_row = vec![]; let mut in_header = true; let column_alignments = alignment .iter() .map(|a| Self::convert_alignment(a)) .collect(); loop { if self.eof() { break; } let (current, _source_range) = self.current().unwrap(); match current { Event::Start(Tag::TableHead) | Event::Start(Tag::TableRow) | Event::End(TagEnd::TableCell) => { self.cursor += 1; } Event::Start(Tag::TableCell) => { self.cursor += 1; let cell_contents = self.parse_text(false); current_row.push(cell_contents); } Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => { self.cursor += 1; let new_row = std::mem::replace(&mut current_row, vec![]); if in_header { header.children = new_row; in_header = false; } else { let row = ParsedMarkdownTableRow::with_children(new_row); body.push(row); } } Event::End(TagEnd::Table) => { self.cursor += 1; break; } _ => { break; } } } ParsedMarkdownTable { source_range, header, body, column_alignments, } } fn convert_alignment(alignment: &Alignment) -> ParsedMarkdownTableAlignment { match alignment { Alignment::None => ParsedMarkdownTableAlignment::None, Alignment::Left => ParsedMarkdownTableAlignment::Left, Alignment::Center => ParsedMarkdownTableAlignment::Center, Alignment::Right => ParsedMarkdownTableAlignment::Right, } } fn parse_list(&mut self, depth: u16, order: Option<u64>) -> ParsedMarkdownList { let (_event, source_range) = self.previous().unwrap(); let source_range = source_range.clone(); let mut children = vec![]; let mut inside_list_item = false; let mut order = order; let mut task_item = None; let mut current_list_items: Vec<Box<ParsedMarkdownElement>> = vec![]; while !self.eof() { let (current, _source_range) = self.current().unwrap(); match current { Event::Start(Tag::List(order)) => { let order = *order; self.cursor += 1; let inner_list = self.parse_list(depth + 1, order); let block = ParsedMarkdownElement::List(inner_list); current_list_items.push(Box::new(block)); } Event::End(TagEnd::List(_)) => { self.cursor += 1; break; } Event::Start(Tag::Item) => { self.cursor += 1; inside_list_item = true; // Check for task list marker (`- [ ]` or `- [x]`) if let Some(next) = self.current() { match next.0 { Event::TaskListMarker(checked) => { task_item = Some(checked); self.cursor += 1; } _ => {} } } if let Some(next) = self.current() { // This is a plain list item. // For example `- some text` or `1. [Docs](./docs.md)` if MarkdownParser::is_text_like(&next.0) { let text = self.parse_text(false); let block = ParsedMarkdownElement::Paragraph(text); current_list_items.push(Box::new(block)); } else { let block = self.parse_block(); if let Some(block) = block { current_list_items.push(Box::new(block)); } } } } Event::End(TagEnd::Item) => { self.cursor += 1; let item_type = if let Some(checked) = task_item { ParsedMarkdownListItemType::Task(checked) } else if let Some(order) = order { ParsedMarkdownListItemType::Ordered(order) } else { ParsedMarkdownListItemType::Unordered }; if let Some(current) = order { order = Some(current + 1); } let contents = std::mem::replace(&mut current_list_items, vec![]); children.push(ParsedMarkdownListItem { contents, depth, item_type, }); inside_list_item = false; task_item = None; } _ => { if !inside_list_item { break; } let block = self.parse_block(); if let Some(block) = block { current_list_items.push(Box::new(block)); } }