1332 lines · 50.1 KB · Rust
RawDownload
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(kind),                    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 1332.