1015 lines · 31.9 KB · Rust
RawDownload
use std::{cell::RefCell, collections::HashMap, mem, ops::Range}; use gpui::{DefiniteLength, FontWeight, SharedString, TextAlign, px, relative};use html5ever::{    Attribute, LocalName, ParseOpts, local_name, parse_document, tendril::TendrilSink,};use markup5ever_rcdom::{Node, NodeData, RcDom};use pulldown_cmark::{Alignment, HeadingLevel};use stacksafe::stacksafe; use crate::html::html_minifier::{Minifier, MinifierOptions}; #[derive(Debug, Clone, Default)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct ParsedHtmlBlock {    pub source_range: Range<usize>,    pub children: Vec<ParsedHtmlElement>,} #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) enum ParsedHtmlElement {    Heading(ParsedHtmlHeading),    List(ParsedHtmlList),    Table(ParsedHtmlTable),    BlockQuote(ParsedHtmlBlockQuote),    Paragraph(ParsedHtmlParagraph),    Image(HtmlImage),} #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct ParsedHtmlParagraph {    pub text_align: Option<TextAlign>,    pub contents: HtmlParagraph,} impl ParsedHtmlElement {    pub fn source_range(&self) -> Option<Range<usize>> {        Some(match self {            Self::Heading(heading) => heading.source_range.clone(),            Self::List(list) => list.source_range.clone(),            Self::Table(table) => table.source_range.clone(),            Self::BlockQuote(block_quote) => block_quote.source_range.clone(),            Self::Paragraph(paragraph) => match paragraph.contents.first()? {                HtmlParagraphChunk::Text(text) => text.source_range.clone(),                HtmlParagraphChunk::Image(image) => image.source_range.clone(),            },            Self::Image(image) => image.source_range.clone(),        })    }} pub(crate) type HtmlParagraph = Vec<HtmlParagraphChunk>; #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) enum HtmlParagraphChunk {    Text(ParsedHtmlText),    Image(HtmlImage),} #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct ParsedHtmlList {    pub source_range: Range<usize>,    pub depth: u16,    pub ordered: bool,    pub items: Vec<ParsedHtmlListItem>,} #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct ParsedHtmlListItem {    pub source_range: Range<usize>,    pub item_type: ParsedHtmlListItemType,    pub content: Vec<ParsedHtmlElement>,} #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) enum ParsedHtmlListItemType {    Ordered(u64),    Unordered,} #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct ParsedHtmlHeading {    pub source_range: Range<usize>,    pub level: HeadingLevel,    pub contents: HtmlParagraph,    pub text_align: Option<TextAlign>,} #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct ParsedHtmlTable {    pub source_range: Range<usize>,    pub header: Vec<ParsedHtmlTableRow>,    pub body: Vec<ParsedHtmlTableRow>,    pub caption: Option<HtmlParagraph>,} #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct ParsedHtmlTableColumn {    pub col_span: usize,    pub row_span: usize,    pub is_header: bool,    pub children: HtmlParagraph,    pub alignment: Alignment,} #[derive(Debug, Clone, Default)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct ParsedHtmlTableRow {    pub columns: Vec<ParsedHtmlTableColumn>,} #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct ParsedHtmlBlockQuote {    pub source_range: Range<usize>,    pub children: Vec<ParsedHtmlElement>,} #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct ParsedHtmlText {    pub source_range: Range<usize>,    pub contents: SharedString,    pub highlights: Vec<(Range<usize>, HtmlHighlightStyle)>,    pub links: Vec<(Range<usize>, SharedString)>,} #[derive(Debug, Clone, Default, PartialEq, Eq)]pub(crate) struct HtmlHighlightStyle {    pub italic: bool,    pub underline: bool,    pub strikethrough: bool,    pub weight: FontWeight,    pub link: bool,    pub oblique: bool,} #[derive(Debug, Clone)]#[cfg_attr(test, derive(PartialEq))]pub(crate) struct HtmlImage {    pub dest_url: SharedString,    pub source_range: Range<usize>,    pub alt_text: Option<SharedString>,    pub width: Option<DefiniteLength>,    pub height: Option<DefiniteLength>,} impl HtmlImage {    fn new(dest_url: String, source_range: Range<usize>) -> Self {        Self {            dest_url: dest_url.into(),            source_range,            alt_text: None,            width: None,            height: None,        }    }     fn set_alt_text(&mut self, alt_text: SharedString) {        self.alt_text = Some(alt_text);    }     fn set_width(&mut self, width: DefiniteLength) {        self.width = Some(width);    }     fn set_height(&mut self, height: DefiniteLength) {        self.height = Some(height);    }} #[derive(Debug)]struct ParseHtmlNodeContext {    list_item_depth: u16,} impl Default for ParseHtmlNodeContext {    fn default() -> Self {        Self { list_item_depth: 1 }    }} pub(crate) fn parse_html_block(    source: &str,    source_range: Range<usize>,) -> Option<ParsedHtmlBlock> {    let bytes = cleanup_html(source);    let mut cursor = std::io::Cursor::new(bytes);    let dom = parse_document(RcDom::default(), ParseOpts::default())        .from_utf8()        .read_from(&mut cursor)        .ok()?;     let mut children = Vec::new();    parse_html_node(        source_range.clone(),        &dom.document,        &mut children,        &ParseHtmlNodeContext::default(),    );     Some(ParsedHtmlBlock {        source_range,        children,    })} fn cleanup_html(source: &str) -> Vec<u8> {    let mut writer = std::io::Cursor::new(Vec::new());    let mut reader = std::io::Cursor::new(source);    let mut minify = Minifier::new(        &mut writer,        MinifierOptions {            omit_doctype: true,            collapse_whitespace: true,            ..Default::default()        },    );    if let Ok(()) = minify.minify(&mut reader) {        writer.into_inner()    } else {        source.bytes().collect()    }} #[stacksafe]fn parse_html_node(    source_range: Range<usize>,    node: &Node,    elements: &mut Vec<ParsedHtmlElement>,    context: &ParseHtmlNodeContext,) {    match &node.data {        NodeData::Document => {            consume_children(source_range, node, elements, context);        }        NodeData::Text { contents } => {            elements.push(ParsedHtmlElement::Paragraph(ParsedHtmlParagraph {                text_align: None,                contents: vec![HtmlParagraphChunk::Text(ParsedHtmlText {                    source_range,                    highlights: Vec::default(),                    links: Vec::default(),                    contents: contents.borrow().to_string().into(),                })],            }));        }        NodeData::Comment { .. } => {}        NodeData::Element { name, attrs, .. } => {            let styles_map = extract_styles_from_attributes(attrs);            let text_align = text_align_from_attributes(attrs, &styles_map);            let mut styles = if let Some(styles) = html_style_from_html_styles(styles_map) {                vec![styles]            } else {                Vec::default()            };             if name.local == local_name!("img") {                if let Some(image) = extract_image(source_range, attrs) {                    elements.push(ParsedHtmlElement::Image(image));                }            } else if name.local == local_name!("p") {                let mut paragraph = HtmlParagraph::new();                parse_paragraph(                    source_range,                    node,                    &mut paragraph,                    &mut styles,                    &mut Vec::new(),                );                 if !paragraph.is_empty() {                    elements.push(ParsedHtmlElement::Paragraph(ParsedHtmlParagraph {                        text_align,                        contents: paragraph,                    }));                }            } else if matches!(                name.local,                local_name!("h1")                    | local_name!("h2")                    | local_name!("h3")                    | local_name!("h4")                    | local_name!("h5")                    | local_name!("h6")            ) {                let mut paragraph = HtmlParagraph::new();                consume_paragraph(                    source_range.clone(),                    node,                    &mut paragraph,                    &mut styles,                    &mut Vec::new(),                );                 if !paragraph.is_empty() {                    elements.push(ParsedHtmlElement::Heading(ParsedHtmlHeading {                        source_range,                        level: match name.local {                            local_name!("h1") => HeadingLevel::H1,                            local_name!("h2") => HeadingLevel::H2,                            local_name!("h3") => HeadingLevel::H3,                            local_name!("h4") => HeadingLevel::H4,                            local_name!("h5") => HeadingLevel::H5,                            local_name!("h6") => HeadingLevel::H6,                            _ => unreachable!(),                        },                        contents: paragraph,                        text_align,                    }));                }            } else if name.local == local_name!("ul") || name.local == local_name!("ol") {                if let Some(list) = extract_html_list(                    node,                    name.local == local_name!("ol"),                    context.list_item_depth,                    source_range,                ) {                    elements.push(ParsedHtmlElement::List(list));                }            } else if name.local == local_name!("blockquote") {                if let Some(blockquote) = extract_html_blockquote(node, source_range) {                    elements.push(ParsedHtmlElement::BlockQuote(blockquote));                }            } else if name.local == local_name!("table") {                if let Some(table) = extract_html_table(node, source_range) {                    elements.push(ParsedHtmlElement::Table(table));                }            } else {                consume_children(source_range, node, elements, context);            }        }        _ => {}    }} #[stacksafe]fn parse_paragraph(    source_range: Range<usize>,    node: &Node,    paragraph: &mut HtmlParagraph,    highlights: &mut Vec<HtmlHighlightStyle>,    links: &mut Vec<SharedString>,) {    fn items_with_range<T>(        range: Range<usize>,        items: impl IntoIterator<Item = T>,    ) -> Vec<(Range<usize>, T)> {        items            .into_iter()            .map(|item| (range.clone(), item))            .collect()    }     match &node.data {        NodeData::Text { contents } => {            if let Some(text) =                paragraph                    .iter_mut()                    .last()                    .and_then(|paragraph_chunk| match paragraph_chunk {                        HtmlParagraphChunk::Text(text) => Some(text),                        _ => None,                    })            {                let mut new_text = text.contents.to_string();                new_text.push_str(&contents.borrow());                 text.highlights.extend(items_with_range(                    text.contents.len()..new_text.len(),                    mem::take(highlights),                ));                text.links.extend(items_with_range(                    text.contents.len()..new_text.len(),                    mem::take(links),                ));                text.contents = SharedString::from(new_text);            } else {                let contents = contents.borrow().to_string();                paragraph.push(HtmlParagraphChunk::Text(ParsedHtmlText {                    source_range,                    highlights: items_with_range(0..contents.len(), mem::take(highlights)),                    links: items_with_range(0..contents.len(), mem::take(links)),                    contents: contents.into(),                }));            }        }        NodeData::Element { name, attrs, .. } => {            if name.local == local_name!("img") {                if let Some(image) = extract_image(source_range, attrs) {                    paragraph.push(HtmlParagraphChunk::Image(image));                }            } else if name.local == local_name!("b") || name.local == local_name!("strong") {                highlights.push(HtmlHighlightStyle {                    weight: FontWeight::BOLD,                    ..Default::default()                });                consume_paragraph(source_range, node, paragraph, highlights, links);            } else if name.local == local_name!("i") {                highlights.push(HtmlHighlightStyle {                    italic: true,                    ..Default::default()                });                consume_paragraph(source_range, node, paragraph, highlights, links);            } else if name.local == local_name!("em") {                highlights.push(HtmlHighlightStyle {                    oblique: true,                    ..Default::default()                });                consume_paragraph(source_range, node, paragraph, highlights, links);            } else if name.local == local_name!("del") {                highlights.push(HtmlHighlightStyle {                    strikethrough: true,                    ..Default::default()                });                consume_paragraph(source_range, node, paragraph, highlights, links);            } else if name.local == local_name!("ins") {                highlights.push(HtmlHighlightStyle {                    underline: true,                    ..Default::default()                });                consume_paragraph(source_range, node, paragraph, highlights, links);            } else if name.local == local_name!("a") {                if let Some(url) = attr_value(attrs, local_name!("href")) {                    highlights.push(HtmlHighlightStyle {                        link: true,                        ..Default::default()                    });                    links.push(url.into());                }                consume_paragraph(source_range, node, paragraph, highlights, links);            } else {                consume_paragraph(source_range, node, paragraph, highlights, links);            }        }        _ => {}    }} fn consume_paragraph(    source_range: Range<usize>,    node: &Node,    paragraph: &mut HtmlParagraph,    highlights: &mut Vec<HtmlHighlightStyle>,    links: &mut Vec<SharedString>,) {    for child in node.children.borrow().iter() {        parse_paragraph(source_range.clone(), child, paragraph, highlights, links);    }} fn parse_table_row(source_range: Range<usize>, node: &Node) -> Option<ParsedHtmlTableRow> {    let mut columns = Vec::new();     if let NodeData::Element { name, .. } = &node.data {        if name.local != local_name!("tr") {            return None;        }         for child in node.children.borrow().iter() {            if let Some(column) = parse_table_column(source_range.clone(), child) {                columns.push(column);            }        }    }     if columns.is_empty() {        None    } else {        Some(ParsedHtmlTableRow { columns })    }} fn parse_table_column(source_range: Range<usize>, node: &Node) -> Option<ParsedHtmlTableColumn> {    match &node.data {        NodeData::Element { name, attrs, .. } => {            if !matches!(name.local, local_name!("th") | local_name!("td")) {                return None;            }             let mut children = HtmlParagraph::new();            consume_paragraph(                source_range,                node,                &mut children,                &mut Vec::new(),                &mut Vec::new(),            );             let is_header = name.local == local_name!("th"); 

Lines 1–500 of 1015.