use collections::{BTreeMap, HashMap, HashSet};use gpui::SharedString;use linkify::LinkFinder;pub use pulldown_cmark::TagEnd as MarkdownTagEnd;use pulldown_cmark::{ Alignment, CowStr, HeadingLevel, LinkType, MetadataBlockKind, Options, Parser,};use std::{ops::Range, sync::Arc};use util::markdown::generate_heading_slug; use crate::{html, path_range::PathWithRange}; pub const PARSE_OPTIONS: Options = Options::ENABLE_TABLES .union(Options::ENABLE_FOOTNOTES) .union(Options::ENABLE_STRIKETHROUGH) .union(Options::ENABLE_TASKLISTS) .union(Options::ENABLE_SMART_PUNCTUATION) .union(Options::ENABLE_HEADING_ATTRIBUTES) .union(Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS) .union(Options::ENABLE_OLD_FOOTNOTES) .union(Options::ENABLE_GFM) .union(Options::ENABLE_SUPERSCRIPT) .union(Options::ENABLE_SUBSCRIPT); #[derive(Default)]struct ParseState { events: Vec<(Range<usize>, MarkdownEvent)>, root_block_starts: Vec<usize>, depth: usize,} #[derive(Debug, Default)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct ParsedMarkdownData { pub events: Vec<(Range<usize>, MarkdownEvent)>, pub language_names: HashSet<SharedString>, pub language_paths: HashSet<Arc<str>>, pub root_block_starts: Vec<usize>, pub html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>, pub heading_slugs: HashMap<SharedString, usize>, pub footnote_definitions: HashMap<SharedString, usize>,} impl ParseState { fn push_event(&mut self, range: Range<usize>, event: MarkdownEvent) { match &event { MarkdownEvent::Start(_) => { if self.depth == 0 { self.root_block_starts.push(range.start); self.events.push((range.clone(), MarkdownEvent::RootStart)); } self.depth += 1; self.events.push((range, event)); } MarkdownEvent::End(_) => { self.events.push((range.clone(), event)); if self.depth > 0 { self.depth -= 1; if self.depth == 0 { let root_block_index = self.root_block_starts.len() - 1; self.events .push((range, MarkdownEvent::RootEnd(root_block_index))); } } } MarkdownEvent::Rule => { if self.depth == 0 && !range.is_empty() { self.root_block_starts.push(range.start); let root_block_index = self.root_block_starts.len() - 1; self.events.push((range.clone(), MarkdownEvent::RootStart)); self.events.push((range.clone(), event)); self.events .push((range, MarkdownEvent::RootEnd(root_block_index))); } else { self.events.push((range, event)); } } _ => { self.events.push((range, event)); } } }} const MAX_DUPLICATE_HEADING_SLUGS: usize = 128; fn build_heading_slugs( source: &str, events: &[(Range<usize>, MarkdownEvent)],) -> HashMap<SharedString, usize> { let mut slugs = HashMap::default(); let mut slug_counts: HashMap<String, usize> = HashMap::default(); let mut inside_heading = false; let mut heading_text = String::new(); let mut heading_source_start: Option<usize> = None; for (range, event) in events { match event { MarkdownEvent::Start(MarkdownTag::Heading { .. }) => { inside_heading = true; heading_text.clear(); heading_source_start = None; } MarkdownEvent::End(MarkdownTagEnd::Heading(_)) => { if inside_heading { let source_offset = heading_source_start.unwrap_or(range.start); let base_slug = generate_heading_slug(&heading_text); let count = slug_counts.entry(base_slug.clone()).or_insert(0); let mut slug = if *count == 0 { base_slug.clone() } else { format!("{base_slug}-{count}") }; *count += 1; while slugs.contains_key(slug.as_str()) { let Some(count) = slug_counts.get_mut(&base_slug) else { slug.clear(); break; }; if *count >= MAX_DUPLICATE_HEADING_SLUGS { slug.clear(); break; } slug = format!("{base_slug}-{count}"); *count += 1; } if !slug.is_empty() { slugs.insert(SharedString::from(slug), source_offset); } inside_heading = false; } } MarkdownEvent::Text | MarkdownEvent::Code if inside_heading => { if heading_source_start.is_none() { heading_source_start = Some(range.start); } heading_text.push_str(&source[range.clone()]); } MarkdownEvent::SubstitutedText(substituted) if inside_heading => { if heading_source_start.is_none() { heading_source_start = Some(range.start); } heading_text.push_str(substituted); } _ => {} } } slugs} pub(crate) fn parse_markdown_with_options( text: &str, parse_html: bool, parse_heading_slugs: 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 within_link = false; let mut within_code_block = false; let mut within_metadata = false; 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 { .. }) = pulldown_event { within_metadata = false; } continue; } match pulldown_event { pulldown_cmark::Event::Start(tag) => { if let pulldown_cmark::Tag::HtmlBlock = &tag { state.push_event(range.clone(), MarkdownEvent::Start(MarkdownTag::HtmlBlock)); if parse_html { if let Some(block) = html::html_parser::parse_html_block(&text[range.clone()], range.clone()) { html_blocks.insert(range.start, block); while let Some((event, end_range)) = parser.next() { if let pulldown_cmark::Event::End( pulldown_cmark::TagEnd::HtmlBlock, ) = event { state.push_event( end_range, MarkdownEvent::End(MarkdownTagEnd::HtmlBlock), ); break; } } } } continue; } let tag = match tag { pulldown_cmark::Tag::Link { link_type, dest_url, title, id, } => { within_link = true; MarkdownTag::Link { link_type, dest_url: SharedString::from(dest_url.into_string()), title: SharedString::from(title.into_string()), id: SharedString::from(id.into_string()), } } pulldown_cmark::Tag::MetadataBlock(_kind) => { within_metadata = true; continue; } pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Indented) => { within_code_block = true; MarkdownTag::CodeBlock { kind: CodeBlockKind::Indented, metadata: CodeBlockMetadata { content_range: range.clone(), line_count: 1, }, } } pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced( ref info, )) => { within_code_block = true; let content_range = extract_code_block_content_range(&text[range.clone()]); let content_range = content_range.start + range.start..content_range.end + range.start; // Valid to use bytes since multi-byte UTF-8 doesn't use ASCII chars. let line_count = text[content_range.clone()] .bytes() .filter(|c| *c == b'\n') .count(); let metadata = CodeBlockMetadata { content_range, line_count, }; let info = info.trim(); let kind = if info.is_empty() { CodeBlockKind::Fenced // Languages should never contain a slash, and PathRanges always should. // (Models are told to specify them relative to a workspace root.) } else if info.contains('/') { let path_range = PathWithRange::new(info); language_paths.insert(path_range.path.clone()); CodeBlockKind::FencedSrc(path_range) } else { let language = SharedString::from(info.to_string()); language_names.insert(language.clone()); CodeBlockKind::FencedLang(language) }; MarkdownTag::CodeBlock { kind, metadata } } pulldown_cmark::Tag::Paragraph => MarkdownTag::Paragraph, pulldown_cmark::Tag::Heading { level, id, classes, attrs, } => { let id = id.map(|id| SharedString::from(id.into_string())); let classes = classes .into_iter() .map(|c| SharedString::from(c.into_string())) .collect(); let attrs = attrs .into_iter() .map(|(key, value)| { ( SharedString::from(key.into_string()), value.map(|v| SharedString::from(v.into_string())), ) }) .collect(); MarkdownTag::Heading { level, id, classes, attrs, } } pulldown_cmark::Tag::BlockQuote(_kind) => MarkdownTag::BlockQuote, pulldown_cmark::Tag::List(start_number) => MarkdownTag::List(start_number), pulldown_cmark::Tag::Item => MarkdownTag::Item, pulldown_cmark::Tag::FootnoteDefinition(label) => { MarkdownTag::FootnoteDefinition(SharedString::from(label.to_string())) } pulldown_cmark::Tag::Table(alignments) => MarkdownTag::Table(alignments), pulldown_cmark::Tag::TableHead => MarkdownTag::TableHead, pulldown_cmark::Tag::TableRow => MarkdownTag::TableRow, pulldown_cmark::Tag::TableCell => MarkdownTag::TableCell, pulldown_cmark::Tag::Emphasis => MarkdownTag::Emphasis, pulldown_cmark::Tag::Strong => MarkdownTag::Strong, pulldown_cmark::Tag::Strikethrough => MarkdownTag::Strikethrough, pulldown_cmark::Tag::Superscript => MarkdownTag::Superscript, pulldown_cmark::Tag::Subscript => MarkdownTag::Subscript, pulldown_cmark::Tag::Image { link_type, dest_url, title, id, } => MarkdownTag::Image { link_type, dest_url: SharedString::from(dest_url.into_string()), title: SharedString::from(title.into_string()), id: SharedString::from(id.into_string()), }, pulldown_cmark::Tag::HtmlBlock => MarkdownTag::HtmlBlock, // this is handled above separately pulldown_cmark::Tag::DefinitionList => MarkdownTag::DefinitionList, pulldown_cmark::Tag::DefinitionListTitle => MarkdownTag::DefinitionListTitle, pulldown_cmark::Tag::DefinitionListDefinition => { MarkdownTag::DefinitionListDefinition } }; state.push_event(range, MarkdownEvent::Start(tag)) } pulldown_cmark::Event::End(tag) => { if let pulldown_cmark::TagEnd::Link = tag { within_link = false; } else if let pulldown_cmark::TagEnd::CodeBlock = tag { within_code_block = false; } state.push_event(range, MarkdownEvent::End(tag)); } pulldown_cmark::Event::Text(parsed) => { fn event_for( text: &str, range: Range<usize>, str: &str, ) -> (Range<usize>, MarkdownEvent) { if str == &text[range.clone()] { (range, MarkdownEvent::Text) } else { (range, MarkdownEvent::SubstitutedText(str.to_owned())) } } if within_code_block { let (range, event) = event_for(text, range, &parsed); state.push_event(range, event); continue; } #[derive(Debug)] struct TextRange<'a> { source_range: Range<usize>, merged_range: Range<usize>, parsed: CowStr<'a>, } let mut last_len = parsed.len(); let mut ranges = vec![TextRange { source_range: range.clone(), merged_range: 0..last_len, parsed, }]; while matches!(parser.peek(), Some((pulldown_cmark::Event::Text(_), _))) || (parse_html && matches!( parser.peek(), Some((pulldown_cmark::Event::InlineHtml(_), _)) )) { let Some((next_event, next_range)) = parser.next() else { unreachable!() }; let next_text = match next_event { pulldown_cmark::Event::Text(next_event) => next_event, pulldown_cmark::Event::InlineHtml(_) => CowStr::Borrowed(""), _ => unreachable!(), }; let next_len = last_len + next_text.len(); ranges.push(TextRange { source_range: next_range.clone(), merged_range: last_len..next_len, parsed: next_text, }); last_len = next_len; } let mut merged_text = String::with_capacity(ranges.last().unwrap().merged_range.end); for range in &ranges { merged_text.push_str(&range.parsed); } let mut ranges = ranges.into_iter().peekable(); if !within_link && !within_code_block { let mut finder = LinkFinder::new(); finder.kinds(&[linkify::LinkKind::Url]); // Find links in the merged text for link in finder.links(&merged_text) { let link_start_in_merged = link.start(); let link_end_in_merged = link.end(); while ranges .peek() .is_some_and(|range| range.merged_range.end <= link_start_in_merged) { let range = ranges.next().unwrap(); let (range, event) = event_for(text, range.source_range, &range.parsed); state.push_event(range, event); } let Some(range) = ranges.peek_mut() else { continue; }; let prefix_len = link_start_in_merged - range.merged_range.start; if prefix_len > 0 { let (head, tail) = range.parsed.split_at(prefix_len); let (event_range, event) = event_for( text, range.source_range.start..range.source_range.start + prefix_len, head, ); state.push_event(event_range, event); range.parsed = CowStr::Boxed(tail.into()); range.merged_range.start += prefix_len; range.source_range.start += prefix_len; } let link_start_in_source = range.source_range.start; let mut link_end_in_source = range.source_range.end; let mut link_events = Vec::new(); while ranges .peek() .is_some_and(|range| range.merged_range.end <= link_end_in_merged) { let range = ranges.next().unwrap(); link_end_in_source = range.source_range.end; link_events.push(event_for(text, range.source_range, &range.parsed)); } if let Some(range) = ranges.peek_mut() { let prefix_len = link_end_in_merged - range.merged_range.start; if prefix_len > 0 { let (head, tail) = range.parsed.split_at(prefix_len); link_events.push(event_for( text, range.source_range.start..range.source_range.start + prefix_len, head, )); range.parsed = CowStr::Boxed(tail.into()); range.merged_range.start += prefix_len; range.source_range.start += prefix_len; link_end_in_source = range.source_range.start; } } let link_range = link_start_in_source..link_end_in_source; state.push_event( link_range.clone(), MarkdownEvent::Start(MarkdownTag::Link { link_type: LinkType::Autolink, dest_url: SharedString::from(link.as_str().to_string()), title: SharedString::default(), id: SharedString::default(), }), ); for (range, event) in link_events { state.push_event(range, event); } state.push_event( link_range.clone(), MarkdownEvent::End(MarkdownTagEnd::Link), ); } } for range in ranges { let (range, event) = event_for(text, range.source_range, &range.parsed); state.push_event(range, event); } } pulldown_cmark::Event::Code(_) => { let content_range = extract_code_content_range(&text[range.clone()]); let content_range = content_range.start + range.start..content_range.end + range.start; state.push_event(content_range, MarkdownEvent::Code) } pulldown_cmark::Event::Html(_) => state.push_event(range, MarkdownEvent::Html), pulldown_cmark::Event::InlineHtml(_) => {Lines 1–500 of 1303. Load more
use collections::{BTreeMap, HashMap, HashSet}; use gpui::SharedString; use linkify::LinkFinder; pub use pulldown_cmark::TagEnd as MarkdownTagEnd; use pulldown_cmark::{ Alignment, CowStr, HeadingLevel, LinkType, MetadataBlockKind, Options, Parser, }; use std::{ops::Range, sync::Arc}; use util::markdown::generate_heading_slug; use crate::{html, path_range::PathWithRange}; pub const PARSE_OPTIONS: Options = Options::ENABLE_TABLES .union(Options::ENABLE_FOOTNOTES) .union(Options::ENABLE_STRIKETHROUGH) .union(Options::ENABLE_TASKLISTS) .union(Options::ENABLE_SMART_PUNCTUATION) .union(Options::ENABLE_HEADING_ATTRIBUTES) .union(Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS) .union(Options::ENABLE_OLD_FOOTNOTES) .union(Options::ENABLE_GFM) .union(Options::ENABLE_SUPERSCRIPT) .union(Options::ENABLE_SUBSCRIPT); #[derive(Default)] struct ParseState { events: Vec<(Range<usize>, MarkdownEvent)>, root_block_starts: Vec<usize>, depth: usize, } #[derive(Debug, Default)] #[cfg_attr(test, derive(PartialEq))] pub(crate) struct ParsedMarkdownData { pub events: Vec<(Range<usize>, MarkdownEvent)>, pub language_names: HashSet<SharedString>, pub language_paths: HashSet<Arc<str>>, pub root_block_starts: Vec<usize>, pub html_blocks: BTreeMap<usize, html::html_parser::ParsedHtmlBlock>, pub heading_slugs: HashMap<SharedString, usize>, pub footnote_definitions: HashMap<SharedString, usize>, } impl ParseState { fn push_event(&mut self, range: Range<usize>, event: MarkdownEvent) { match &event { MarkdownEvent::Start(_) => { if self.depth == 0 { self.root_block_starts.push(range.start); self.events.push((range.clone(), MarkdownEvent::RootStart)); } self.depth += 1; self.events.push((range, event)); } MarkdownEvent::End(_) => { self.events.push((range.clone(), event)); if self.depth > 0 { self.depth -= 1; if self.depth == 0 { let root_block_index = self.root_block_starts.len() - 1; self.events .push((range, MarkdownEvent::RootEnd(root_block_index))); } } } MarkdownEvent::Rule => { if self.depth == 0 && !range.is_empty() { self.root_block_starts.push(range.start); let root_block_index = self.root_block_starts.len() - 1; self.events.push((range.clone(), MarkdownEvent::RootStart)); self.events.push((range.clone(), event)); self.events .push((range, MarkdownEvent::RootEnd(root_block_index))); } else { self.events.push((range, event)); } } _ => { self.events.push((range, event)); } } } } const MAX_DUPLICATE_HEADING_SLUGS: usize = 128; fn build_heading_slugs( source: &str, events: &[(Range<usize>, MarkdownEvent)], ) -> HashMap<SharedString, usize> { let mut slugs = HashMap::default(); let mut slug_counts: HashMap<String, usize> = HashMap::default(); let mut inside_heading = false; let mut heading_text = String::new(); let mut heading_source_start: Option<usize> = None; for (range, event) in events { match event { MarkdownEvent::Start(MarkdownTag::Heading { .. }) => { inside_heading = true; heading_text.clear(); heading_source_start = None; } MarkdownEvent::End(MarkdownTagEnd::Heading(_)) => { if inside_heading { let source_offset = heading_source_start.unwrap_or(range.start); let base_slug = generate_heading_slug(&heading_text); let count = slug_counts.entry(base_slug.clone()).or_insert(0); let mut slug = if *count == 0 { base_slug.clone() } else { format!("{base_slug}-{count}") }; *count += 1; while slugs.contains_key(slug.as_str()) { let Some(count) = slug_counts.get_mut(&base_slug) else { slug.clear(); break; }; if *count >= MAX_DUPLICATE_HEADING_SLUGS { slug.clear(); break; } slug = format!("{base_slug}-{count}"); *count += 1; } if !slug.is_empty() { slugs.insert(SharedString::from(slug), source_offset); } inside_heading = false; } } MarkdownEvent::Text | MarkdownEvent::Code if inside_heading => { if heading_source_start.is_none() { heading_source_start = Some(range.start); } heading_text.push_str(&source[range.clone()]); } MarkdownEvent::SubstitutedText(substituted) if inside_heading => { if heading_source_start.is_none() { heading_source_start = Some(range.start); } heading_text.push_str(substituted); } _ => {} } } slugs } pub(crate) fn parse_markdown_with_options( text: &str, parse_html: bool, parse_heading_slugs: 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 within_link = false; let mut within_code_block = false; let mut within_metadata = false; 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 { .. }) = pulldown_event { within_metadata = false; } continue; } match pulldown_event { pulldown_cmark::Event::Start(tag) => { if let pulldown_cmark::Tag::HtmlBlock = &tag { state.push_event(range.clone(), MarkdownEvent::Start(MarkdownTag::HtmlBlock)); if parse_html { if let Some(block) = html::html_parser::parse_html_block(&text[range.clone()], range.clone()) { html_blocks.insert(range.start, block); while let Some((event, end_range)) = parser.next() { if let pulldown_cmark::Event::End( pulldown_cmark::TagEnd::HtmlBlock, ) = event { state.push_event( end_range, MarkdownEvent::End(MarkdownTagEnd::HtmlBlock), ); break; } } } } continue; } let tag = match tag { pulldown_cmark::Tag::Link { link_type, dest_url, title, id, } => { within_link = true; MarkdownTag::Link { link_type, dest_url: SharedString::from(dest_url.into_string()), title: SharedString::from(title.into_string()), id: SharedString::from(id.into_string()), } } pulldown_cmark::Tag::MetadataBlock(_kind) => { within_metadata = true; continue; } pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Indented) => { within_code_block = true; MarkdownTag::CodeBlock { kind: CodeBlockKind::Indented, metadata: CodeBlockMetadata { content_range: range.clone(), line_count: 1, }, } } pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced( ref info, )) => { within_code_block = true; let content_range = extract_code_block_content_range(&text[range.clone()]); let content_range = content_range.start + range.start..content_range.end + range.start; // Valid to use bytes since multi-byte UTF-8 doesn't use ASCII chars. let line_count = text[content_range.clone()] .bytes() .filter(|c| *c == b'\n') .count(); let metadata = CodeBlockMetadata { content_range, line_count, }; let info = info.trim(); let kind = if info.is_empty() { CodeBlockKind::Fenced // Languages should never contain a slash, and PathRanges always should. // (Models are told to specify them relative to a workspace root.) } else if info.contains('/') { let path_range = PathWithRange::new(info); language_paths.insert(path_range.path.clone()); CodeBlockKind::FencedSrc(path_range) } else { let language = SharedString::from(info.to_string()); language_names.insert(language.clone()); CodeBlockKind::FencedLang(language) }; MarkdownTag::CodeBlock { kind, metadata } } pulldown_cmark::Tag::Paragraph => MarkdownTag::Paragraph, pulldown_cmark::Tag::Heading { level, id, classes, attrs, } => { let id = id.map(|id| SharedString::from(id.into_string())); let classes = classes .into_iter() .map(|c| SharedString::from(c.into_string())) .collect(); let attrs = attrs .into_iter() .map(|(key, value)| { ( SharedString::from(key.into_string()), value.map(|v| SharedString::from(v.into_string())), ) }) .collect(); MarkdownTag::Heading { level, id, classes, attrs, } } pulldown_cmark::Tag::BlockQuote(_kind) => MarkdownTag::BlockQuote, pulldown_cmark::Tag::List(start_number) => MarkdownTag::List(start_number), pulldown_cmark::Tag::Item => MarkdownTag::Item, pulldown_cmark::Tag::FootnoteDefinition(label) => { MarkdownTag::FootnoteDefinition(SharedString::from(label.to_string())) } pulldown_cmark::Tag::Table(alignments) => MarkdownTag::Table(alignments), pulldown_cmark::Tag::TableHead => MarkdownTag::TableHead, pulldown_cmark::Tag::TableRow => MarkdownTag::TableRow, pulldown_cmark::Tag::TableCell => MarkdownTag::TableCell, pulldown_cmark::Tag::Emphasis => MarkdownTag::Emphasis, pulldown_cmark::Tag::Strong => MarkdownTag::Strong, pulldown_cmark::Tag::Strikethrough => MarkdownTag::Strikethrough, pulldown_cmark::Tag::Superscript => MarkdownTag::Superscript, pulldown_cmark::Tag::Subscript => MarkdownTag::Subscript, pulldown_cmark::Tag::Image { link_type, dest_url, title, id, } => MarkdownTag::Image { link_type, dest_url: SharedString::from(dest_url.into_string()), title: SharedString::from(title.into_string()), id: SharedString::from(id.into_string()), }, pulldown_cmark::Tag::HtmlBlock => MarkdownTag::HtmlBlock, // this is handled above separately pulldown_cmark::Tag::DefinitionList => MarkdownTag::DefinitionList, pulldown_cmark::Tag::DefinitionListTitle => MarkdownTag::DefinitionListTitle, pulldown_cmark::Tag::DefinitionListDefinition => { MarkdownTag::DefinitionListDefinition } }; state.push_event(range, MarkdownEvent::Start(tag)) } pulldown_cmark::Event::End(tag) => { if let pulldown_cmark::TagEnd::Link = tag { within_link = false; } else if let pulldown_cmark::TagEnd::CodeBlock = tag { within_code_block = false; } state.push_event(range, MarkdownEvent::End(tag)); } pulldown_cmark::Event::Text(parsed) => { fn event_for( text: &str, range: Range<usize>, str: &str, ) -> (Range<usize>, MarkdownEvent) { if str == &text[range.clone()] { (range, MarkdownEvent::Text) } else { (range, MarkdownEvent::SubstitutedText(str.to_owned())) } } if within_code_block { let (range, event) = event_for(text, range, &parsed); state.push_event(range, event); continue; } #[derive(Debug)] struct TextRange<'a> { source_range: Range<usize>, merged_range: Range<usize>, parsed: CowStr<'a>, } let mut last_len = parsed.len(); let mut ranges = vec![TextRange { source_range: range.clone(), merged_range: 0..last_len, parsed, }]; while matches!(parser.peek(), Some((pulldown_cmark::Event::Text(_), _))) || (parse_html && matches!( parser.peek(), Some((pulldown_cmark::Event::InlineHtml(_), _)) )) { let Some((next_event, next_range)) = parser.next() else { unreachable!() }; let next_text = match next_event { pulldown_cmark::Event::Text(next_event) => next_event, pulldown_cmark::Event::InlineHtml(_) => CowStr::Borrowed(""), _ => unreachable!(), }; let next_len = last_len + next_text.len(); ranges.push(TextRange { source_range: next_range.clone(), merged_range: last_len..next_len, parsed: next_text, }); last_len = next_len; } let mut merged_text = String::with_capacity(ranges.last().unwrap().merged_range.end); for range in &ranges { merged_text.push_str(&range.parsed); } let mut ranges = ranges.into_iter().peekable(); if !within_link && !within_code_block { let mut finder = LinkFinder::new(); finder.kinds(&[linkify::LinkKind::Url]); // Find links in the merged text for link in finder.links(&merged_text) { let link_start_in_merged = link.start(); let link_end_in_merged = link.end(); while ranges .peek() .is_some_and(|range| range.merged_range.end <= link_start_in_merged) { let range = ranges.next().unwrap(); let (range, event) = event_for(text, range.source_range, &range.parsed); state.push_event(range, event); } let Some(range) = ranges.peek_mut() else { continue; }; let prefix_len = link_start_in_merged - range.merged_range.start; if prefix_len > 0 { let (head, tail) = range.parsed.split_at(prefix_len); let (event_range, event) = event_for( text, range.source_range.start..range.source_range.start + prefix_len, head, ); state.push_event(event_range, event); range.parsed = CowStr::Boxed(tail.into()); range.merged_range.start += prefix_len; range.source_range.start += prefix_len; } let link_start_in_source = range.source_range.start; let mut link_end_in_source = range.source_range.end; let mut link_events = Vec::new(); while ranges .peek() .is_some_and(|range| range.merged_range.end <= link_end_in_merged) { let range = ranges.next().unwrap(); link_end_in_source = range.source_range.end; link_events.push(event_for(text, range.source_range, &range.parsed)); } if let Some(range) = ranges.peek_mut() { let prefix_len = link_end_in_merged - range.merged_range.start; if prefix_len > 0 { let (head, tail) = range.parsed.split_at(prefix_len); link_events.push(event_for( text, range.source_range.start..range.source_range.start + prefix_len, head, )); range.parsed = CowStr::Boxed(tail.into()); range.merged_range.start += prefix_len; range.source_range.start += prefix_len; link_end_in_source = range.source_range.start; } } let link_range = link_start_in_source..link_end_in_source; state.push_event( link_range.clone(), MarkdownEvent::Start(MarkdownTag::Link { link_type: LinkType::Autolink, dest_url: SharedString::from(link.as_str().to_string()), title: SharedString::default(), id: SharedString::default(), }), ); for (range, event) in link_events { state.push_event(range, event); } state.push_event( link_range.clone(), MarkdownEvent::End(MarkdownTagEnd::Link), ); } } for range in ranges { let (range, event) = event_for(text, range.source_range, &range.parsed); state.push_event(range, event); } } pulldown_cmark::Event::Code(_) => { let content_range = extract_code_content_range(&text[range.clone()]); let content_range = content_range.start + range.start..content_range.end + range.start; state.push_event(content_range, MarkdownEvent::Code) } pulldown_cmark::Event::Html(_) => state.push_event(range, MarkdownEvent::Html), pulldown_cmark::Event::InlineHtml(_) => {