use crate::html_element::HtmlElement; use crate::markdown_writer::{HandleTag, HandlerOutcome, MarkdownWriter, StartTagOutcome}; pub struct RustdocHeadingHandler; impl HandleTag for RustdocHeadingHandler { fn should_handle(&self, _tag: &str) -> bool { // We're only handling text, so we don't need to visit any tags. false } fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { if writer.is_inside("h1") || writer.is_inside("h2") || writer.is_inside("h3") || writer.is_inside("h4") || writer.is_inside("h5") || writer.is_inside("h6") { let text = text .trim_matches(|char| char == '\n' || char == '\r' || char == 'ยง') .replace('\n', " "); writer.push_str(&text); return HandlerOutcome::Handled; } HandlerOutcome::NoOp } } pub struct RustdocCodeHandler; impl HandleTag for RustdocCodeHandler { fn should_handle(&self, tag: &str) -> bool { match tag { "pre" | "code" => true, _ => false, } } fn handle_tag_start( &mut self, tag: &HtmlElement, writer: &mut MarkdownWriter, ) -> StartTagOutcome { match tag.tag.as_str() { "code" => { if !writer.is_inside("pre") { writer.push_str("`"); } } "pre" => { let classes = tag.classes(); let is_rust = classes.iter().any(|class| class == "rust"); let language = is_rust .then(|| "rs") .or_else(|| { classes.iter().find_map(|class| { if let Some((_, language)) = class.split_once("language-") { Some(language.trim()) } else { None } }) }) .unwrap_or(""); writer.push_str(&format!("\n\n```{language}\n")); } _ => {} } StartTagOutcome::Continue } fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { match tag.tag.as_str() { "code" => { if !writer.is_inside("pre") { writer.push_str("`"); } } "pre" => writer.push_str("\n```\n"), _ => {} } } fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { if writer.is_inside("pre") { writer.push_str(&text); return HandlerOutcome::Handled; } HandlerOutcome::NoOp } } const RUSTDOC_ITEM_NAME_CLASS: &str = "item-name"; pub struct RustdocItemHandler; impl RustdocItemHandler { /// Returns whether we're currently inside of an `.item-name` element, which /// rustdoc uses to display Rust items in a list. fn is_inside_item_name(writer: &MarkdownWriter) -> bool { writer .current_element_stack() .iter() .any(|element| element.has_class(RUSTDOC_ITEM_NAME_CLASS)) } } impl HandleTag for RustdocItemHandler { fn should_handle(&self, tag: &str) -> bool { match tag { "div" | "span" => true, _ => false, } } fn handle_tag_start( &mut self, tag: &HtmlElement, writer: &mut MarkdownWriter, ) -> StartTagOutcome { match tag.tag.as_str() { "div" | "span" => { if Self::is_inside_item_name(writer) && tag.has_class("stab") { writer.push_str(" ["); } } _ => {} } StartTagOutcome::Continue } fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { match tag.tag.as_str() { "div" | "span" => { if tag.has_class(RUSTDOC_ITEM_NAME_CLASS) { writer.push_str(": "); } if Self::is_inside_item_name(writer) && tag.has_class("stab") { writer.push_str("]"); } } _ => {} } } fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { if Self::is_inside_item_name(writer) && !writer.is_inside("span") && !writer.is_inside("code") { writer.push_str(&format!("`{text}`")); return HandlerOutcome::Handled; } HandlerOutcome::NoOp } } pub struct RustdocChromeRemover; impl HandleTag for RustdocChromeRemover { fn should_handle(&self, tag: &str) -> bool { match tag { "head" | "script" | "nav" | "summary" | "button" | "div" | "span" => true, _ => false, } } fn handle_tag_start( &mut self, tag: &HtmlElement, _writer: &mut MarkdownWriter, ) -> StartTagOutcome { match tag.tag.as_str() { "head" | "script" | "nav" => return StartTagOutcome::Skip, "summary" => { if tag.has_class("hideme") { return StartTagOutcome::Skip; } } "button" => { if tag.attr("id").as_deref() == Some("copy-path") { return StartTagOutcome::Skip; } } "div" | "span" => { let classes_to_skip = ["nav-container", "sidebar-elems", "out-of-band"]; if tag.has_any_classes(&classes_to_skip) { return StartTagOutcome::Skip; } } _ => {} } StartTagOutcome::Continue } }