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 collections::HashSet; use crate::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); pub fn parse_markdown( text: &str,) -> ( Vec<(Range<usize>, MarkdownEvent)>, HashSet<SharedString>, HashSet<Arc<str>>,) { let mut events = Vec::new(); let mut language_names = HashSet::default(); let mut language_paths = HashSet::default(); let mut within_link = 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) => { 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; MarkdownTag::MetadataBlock(kind) } pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Indented) => { MarkdownTag::CodeBlock { kind: CodeBlockKind::Indented, metadata: CodeBlockMetadata { content_range: range.clone(), line_count: 1, }, } } pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced( ref info, )) => { 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, pulldown_cmark::Tag::DefinitionList => MarkdownTag::DefinitionList, pulldown_cmark::Tag::DefinitionListTitle => MarkdownTag::DefinitionListTitle, pulldown_cmark::Tag::DefinitionListDefinition => { MarkdownTag::DefinitionListDefinition } }; events.push((range, MarkdownEvent::Start(tag))) } pulldown_cmark::Event::End(tag) => { if let pulldown_cmark::TagEnd::Link = tag { within_link = false; } events.push((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())) } } #[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(_), _))) { let Some((pulldown_cmark::Event::Text(next_event), next_range)) = parser.next() else { unreachable!() }; let next_len = last_len + next_event.len(); ranges.push(TextRange { source_range: next_range.clone(), merged_range: last_len..next_len, parsed: next_event, }); 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 { 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(); events.push(event_for(text, range.source_range, &range.parsed)); } 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); 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; } 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; events.push(( 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(), }), )); events.extend(link_events); events.push((link_range.clone(), MarkdownEvent::End(MarkdownTagEnd::Link))); } } for range in ranges { events.push(event_for(text, range.source_range, &range.parsed)); } } 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; events.push((content_range, MarkdownEvent::Code)) } pulldown_cmark::Event::Html(_) => events.push((range, MarkdownEvent::Html)), pulldown_cmark::Event::InlineHtml(_) => events.push((range, MarkdownEvent::InlineHtml)), pulldown_cmark::Event::FootnoteReference(_) => { events.push((range, MarkdownEvent::FootnoteReference)) } pulldown_cmark::Event::SoftBreak => events.push((range, MarkdownEvent::SoftBreak)), pulldown_cmark::Event::HardBreak => events.push((range, MarkdownEvent::HardBreak)), pulldown_cmark::Event::Rule => events.push((range, MarkdownEvent::Rule)), pulldown_cmark::Event::TaskListMarker(checked) => { events.push((range, MarkdownEvent::TaskListMarker(checked))) } pulldown_cmark::Event::InlineMath(_) | pulldown_cmark::Event::DisplayMath(_) => {} } } (events, language_names, language_paths)} pub fn parse_links_only(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> { let mut events = Vec::new(); let mut finder = LinkFinder::new(); finder.kinds(&[linkify::LinkKind::Url]); let mut text_range = Range { start: 0, end: text.len(), }; for link in finder.links(text) { let link_range = link.start()..link.end(); if link_range.start > text_range.start { events.push((text_range.start..link_range.start, MarkdownEvent::Text)); } events.push(( 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(), }), )); events.push((link_range.clone(), MarkdownEvent::Text)); events.push((link_range.clone(), MarkdownEvent::End(MarkdownTagEnd::Link))); text_range.start = link_range.end; } if text_range.end > text_range.start { events.push((text_range, MarkdownEvent::Text)); } events} /// A static-lifetime equivalent of pulldown_cmark::Event so we can cache the/// parse result for rendering without resorting to unsafe lifetime coercion.#[derive(Clone, Debug, PartialEq)]pub enum MarkdownEvent { /// Start of a tagged element. Events that are yielded after this event /// and before its corresponding `End` event are inside this element. /// Start and end events are guaranteed to be balanced. Start(MarkdownTag), /// End of a tagged element. End(MarkdownTagEnd), /// Text that uses the associated range from the markdown source. Text, /// Text that differs from the markdown source - typically due to substitution of HTML entities /// and smart punctuation. SubstitutedText(String), /// An inline code node. Code, /// An HTML node. Html, /// An inline HTML node. InlineHtml, /// A reference to a footnote with given label, which may or may not be defined /// by an event with a `Tag::FootnoteDefinition` tag. Definitions and references to them may /// occur in any order. FootnoteReference, /// A soft line break. SoftBreak, /// A hard line break. HardBreak, /// A horizontal ruler. Rule, /// A task list marker, rendered as a checkbox in HTML. Contains a true when it is checked. TaskListMarker(bool),} /// Tags for elements that can contain other elements.#[derive(Clone, Debug, PartialEq)]pub enum MarkdownTag { /// A paragraph of text and other inline elements. Paragraph, /// A heading, with optional identifier, classes and custom attributes. /// The identifier is prefixed with `#` and the last one in the attributes /// list is chosen, classes are prefixed with `.` and custom attributes /// have no prefix and can optionally have a value (`myattr` o `myattr=myvalue`). Heading { level: HeadingLevel, id: Option<SharedString>, classes: Vec<SharedString>, /// The first item of the tuple is the attr and second one the value. attrs: Vec<(SharedString, Option<SharedString>)>, }, BlockQuote, /// A code block. CodeBlock { kind: CodeBlockKind, metadata: CodeBlockMetadata, }, /// A HTML block. HtmlBlock, /// A list. If the list is ordered the field indicates the number of the first item. /// Contains only list items. List(Option<u64>), // TODO: add delim and tight for ast (not needed for html) /// A list item. Item, /// A footnote definition. The value contained is the footnote's label by which it can /// be referred to. FootnoteDefinition(SharedString), /// A table. Contains a vector describing the text-alignment for each of its columns. Table(Vec<Alignment>), /// A table header. Contains only `TableCell`s. Note that the table body starts immediately /// after the closure of the `TableHead` tag. There is no `TableBody` tag. TableHead, /// A table row. Is used both for header rows as body rows. Contains only `TableCell`s. TableRow, TableCell, // span-level tags Emphasis, Strong, Strikethrough, Superscript, Subscript, /// A link. Link { link_type: LinkType, dest_url: SharedString, title: SharedString, /// Identifier of reference links, e.g. `world` in the link `[hello][world]`. id: SharedString, }, /// An image. The first field is the link type, the second the destination URL and the third is a title, /// the fourth is the link identifier. Image { link_type: LinkType, dest_url: SharedString, title: SharedString, /// Identifier of reference links, e.g. `world` in the link `[hello][world]`. id: SharedString, }, /// A metadata block. MetadataBlock(MetadataBlockKind), DefinitionList, DefinitionListTitle, DefinitionListDefinition,} #[derive(Clone, Debug, PartialEq)]pub enum CodeBlockKind { Indented, /// "Fenced" means "surrounded by triple backticks." /// There can optionally be either a language after the backticks (like in traditional Markdown) /// or, if an agent is specifying a path for a source location in the project, it can be a PathRange, /// e.g. ```path/to/foo.rs#L123-456 instead of ```rust Fenced, FencedLang(SharedString), FencedSrc(PathWithRange),Lines 1–500 of 837. Load more
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 collections::HashSet; use crate::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); pub fn parse_markdown( text: &str, ) -> ( Vec<(Range<usize>, MarkdownEvent)>, HashSet<SharedString>, HashSet<Arc<str>>, ) { let mut events = Vec::new(); let mut language_names = HashSet::default(); let mut language_paths = HashSet::default(); let mut within_link = 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) => { 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; MarkdownTag::MetadataBlock(kind) } pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Indented) => { MarkdownTag::CodeBlock { kind: CodeBlockKind::Indented, metadata: CodeBlockMetadata { content_range: range.clone(), line_count: 1, }, } } pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced( ref info, )) => { 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, pulldown_cmark::Tag::DefinitionList => MarkdownTag::DefinitionList, pulldown_cmark::Tag::DefinitionListTitle => MarkdownTag::DefinitionListTitle, pulldown_cmark::Tag::DefinitionListDefinition => { MarkdownTag::DefinitionListDefinition } }; events.push((range, MarkdownEvent::Start(tag))) } pulldown_cmark::Event::End(tag) => { if let pulldown_cmark::TagEnd::Link = tag { within_link = false; } events.push((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())) } } #[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(_), _))) { let Some((pulldown_cmark::Event::Text(next_event), next_range)) = parser.next() else { unreachable!() }; let next_len = last_len + next_event.len(); ranges.push(TextRange { source_range: next_range.clone(), merged_range: last_len..next_len, parsed: next_event, }); 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 { 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(); events.push(event_for(text, range.source_range, &range.parsed)); } 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); 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; } 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; events.push(( 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(), }), )); events.extend(link_events); events.push((link_range.clone(), MarkdownEvent::End(MarkdownTagEnd::Link))); } } for range in ranges { events.push(event_for(text, range.source_range, &range.parsed)); } } 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; events.push((content_range, MarkdownEvent::Code)) } pulldown_cmark::Event::Html(_) => events.push((range, MarkdownEvent::Html)), pulldown_cmark::Event::InlineHtml(_) => events.push((range, MarkdownEvent::InlineHtml)), pulldown_cmark::Event::FootnoteReference(_) => { events.push((range, MarkdownEvent::FootnoteReference)) } pulldown_cmark::Event::SoftBreak => events.push((range, MarkdownEvent::SoftBreak)), pulldown_cmark::Event::HardBreak => events.push((range, MarkdownEvent::HardBreak)), pulldown_cmark::Event::Rule => events.push((range, MarkdownEvent::Rule)), pulldown_cmark::Event::TaskListMarker(checked) => { events.push((range, MarkdownEvent::TaskListMarker(checked))) } pulldown_cmark::Event::InlineMath(_) | pulldown_cmark::Event::DisplayMath(_) => {} } } (events, language_names, language_paths) } pub fn parse_links_only(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> { let mut events = Vec::new(); let mut finder = LinkFinder::new(); finder.kinds(&[linkify::LinkKind::Url]); let mut text_range = Range { start: 0, end: text.len(), }; for link in finder.links(text) { let link_range = link.start()..link.end(); if link_range.start > text_range.start { events.push((text_range.start..link_range.start, MarkdownEvent::Text)); } events.push(( 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(), }), )); events.push((link_range.clone(), MarkdownEvent::Text)); events.push((link_range.clone(), MarkdownEvent::End(MarkdownTagEnd::Link))); text_range.start = link_range.end; } if text_range.end > text_range.start { events.push((text_range, MarkdownEvent::Text)); } events } /// A static-lifetime equivalent of pulldown_cmark::Event so we can cache the /// parse result for rendering without resorting to unsafe lifetime coercion. #[derive(Clone, Debug, PartialEq)] pub enum MarkdownEvent { /// Start of a tagged element. Events that are yielded after this event /// and before its corresponding `End` event are inside this element. /// Start and end events are guaranteed to be balanced. Start(MarkdownTag), /// End of a tagged element. End(MarkdownTagEnd), /// Text that uses the associated range from the markdown source. Text, /// Text that differs from the markdown source - typically due to substitution of HTML entities /// and smart punctuation. SubstitutedText(String), /// An inline code node. Code, /// An HTML node. Html, /// An inline HTML node. InlineHtml, /// A reference to a footnote with given label, which may or may not be defined /// by an event with a `Tag::FootnoteDefinition` tag. Definitions and references to them may /// occur in any order. FootnoteReference, /// A soft line break. SoftBreak, /// A hard line break. HardBreak, /// A horizontal ruler. Rule, /// A task list marker, rendered as a checkbox in HTML. Contains a true when it is checked. TaskListMarker(bool), } /// Tags for elements that can contain other elements. #[derive(Clone, Debug, PartialEq)] pub enum MarkdownTag { /// A paragraph of text and other inline elements. Paragraph, /// A heading, with optional identifier, classes and custom attributes. /// The identifier is prefixed with `#` and the last one in the attributes /// list is chosen, classes are prefixed with `.` and custom attributes /// have no prefix and can optionally have a value (`myattr` o `myattr=myvalue`). Heading { level: HeadingLevel, id: Option<SharedString>, classes: Vec<SharedString>, /// The first item of the tuple is the attr and second one the value. attrs: Vec<(SharedString, Option<SharedString>)>, }, BlockQuote, /// A code block. CodeBlock { kind: CodeBlockKind, metadata: CodeBlockMetadata, }, /// A HTML block. HtmlBlock, /// A list. If the list is ordered the field indicates the number of the first item. /// Contains only list items. List(Option<u64>), // TODO: add delim and tight for ast (not needed for html) /// A list item. Item, /// A footnote definition. The value contained is the footnote's label by which it can /// be referred to. FootnoteDefinition(SharedString), /// A table. Contains a vector describing the text-alignment for each of its columns. Table(Vec<Alignment>), /// A table header. Contains only `TableCell`s. Note that the table body starts immediately /// after the closure of the `TableHead` tag. There is no `TableBody` tag. TableHead, /// A table row. Is used both for header rows as body rows. Contains only `TableCell`s. TableRow, TableCell, // span-level tags Emphasis, Strong, Strikethrough, Superscript, Subscript, /// A link. Link { link_type: LinkType, dest_url: SharedString, title: SharedString, /// Identifier of reference links, e.g. `world` in the link `[hello][world]`. id: SharedString, }, /// An image. The first field is the link type, the second the destination URL and the third is a title, /// the fourth is the link identifier. Image { link_type: LinkType, dest_url: SharedString, title: SharedString, /// Identifier of reference links, e.g. `world` in the link `[hello][world]`. id: SharedString, }, /// A metadata block. MetadataBlock(MetadataBlockKind), DefinitionList, DefinitionListTitle, DefinitionListDefinition, } #[derive(Clone, Debug, PartialEq)] pub enum CodeBlockKind { Indented, /// "Fenced" means "surrounded by triple backticks." /// There can optionally be either a language after the backticks (like in traditional Markdown) /// or, if an agent is specifying a path for a source location in the project, it can be a PathRange, /// e.g. ```path/to/foo.rs#L123-456 instead of ```rust Fenced, FencedLang(SharedString), FencedSrc(PathWithRange),