rustdoc.rs

  1use crate::html_element::HtmlElement;
  2use crate::markdown_writer::{HandleTag, HandlerOutcome, MarkdownWriter, StartTagOutcome};
  3
  4pub struct RustdocHeadingHandler;
  5
  6impl HandleTag for RustdocHeadingHandler {
  7    fn should_handle(&self, _tag: &str) -> bool {
  8        // We're only handling text, so we don't need to visit any tags.
  9        false
 10    }
 11
 12    fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
 13        if writer.is_inside("h1")
 14            || writer.is_inside("h2")
 15            || writer.is_inside("h3")
 16            || writer.is_inside("h4")
 17            || writer.is_inside("h5")
 18            || writer.is_inside("h6")
 19        {
 20            let text = text
 21                .trim_matches(|char| char == '\n' || char == '\r' || char == 'ยง')
 22                .replace('\n', " ");
 23            writer.push_str(&text);
 24
 25            return HandlerOutcome::Handled;
 26        }
 27
 28        HandlerOutcome::NoOp
 29    }
 30}
 31
 32pub struct RustdocCodeHandler;
 33
 34impl HandleTag for RustdocCodeHandler {
 35    fn should_handle(&self, tag: &str) -> bool {
 36        match tag {
 37            "pre" | "code" => true,
 38            _ => false,
 39        }
 40    }
 41
 42    fn handle_tag_start(
 43        &mut self,
 44        tag: &HtmlElement,
 45        writer: &mut MarkdownWriter,
 46    ) -> StartTagOutcome {
 47        match tag.tag.as_str() {
 48            "code" => {
 49                if !writer.is_inside("pre") {
 50                    writer.push_str("`");
 51                }
 52            }
 53            "pre" => {
 54                let classes = tag.classes();
 55                let is_rust = classes.iter().any(|class| class == "rust");
 56                let language = is_rust
 57                    .then(|| "rs")
 58                    .or_else(|| {
 59                        classes.iter().find_map(|class| {
 60                            if let Some((_, language)) = class.split_once("language-") {
 61                                Some(language.trim())
 62                            } else {
 63                                None
 64                            }
 65                        })
 66                    })
 67                    .unwrap_or("");
 68
 69                writer.push_str(&format!("\n\n```{language}\n"));
 70            }
 71            _ => {}
 72        }
 73
 74        StartTagOutcome::Continue
 75    }
 76
 77    fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
 78        match tag.tag.as_str() {
 79            "code" => {
 80                if !writer.is_inside("pre") {
 81                    writer.push_str("`");
 82                }
 83            }
 84            "pre" => writer.push_str("\n```\n"),
 85            _ => {}
 86        }
 87    }
 88
 89    fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
 90        if writer.is_inside("pre") {
 91            writer.push_str(&text);
 92            return HandlerOutcome::Handled;
 93        }
 94
 95        HandlerOutcome::NoOp
 96    }
 97}
 98
 99const RUSTDOC_ITEM_NAME_CLASS: &str = "item-name";
100
101pub struct RustdocItemHandler;
102
103impl RustdocItemHandler {
104    /// Returns whether we're currently inside of an `.item-name` element, which
105    /// rustdoc uses to display Rust items in a list.
106    fn is_inside_item_name(writer: &MarkdownWriter) -> bool {
107        writer
108            .current_element_stack()
109            .iter()
110            .any(|element| element.has_class(RUSTDOC_ITEM_NAME_CLASS))
111    }
112}
113
114impl HandleTag for RustdocItemHandler {
115    fn should_handle(&self, tag: &str) -> bool {
116        match tag {
117            "div" | "span" => true,
118            _ => false,
119        }
120    }
121
122    fn handle_tag_start(
123        &mut self,
124        tag: &HtmlElement,
125        writer: &mut MarkdownWriter,
126    ) -> StartTagOutcome {
127        match tag.tag.as_str() {
128            "div" | "span" => {
129                if Self::is_inside_item_name(writer) && tag.has_class("stab") {
130                    writer.push_str(" [");
131                }
132            }
133            _ => {}
134        }
135
136        StartTagOutcome::Continue
137    }
138
139    fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
140        match tag.tag.as_str() {
141            "div" | "span" => {
142                if tag.has_class(RUSTDOC_ITEM_NAME_CLASS) {
143                    writer.push_str(": ");
144                }
145
146                if Self::is_inside_item_name(writer) && tag.has_class("stab") {
147                    writer.push_str("]");
148                }
149            }
150            _ => {}
151        }
152    }
153
154    fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
155        if Self::is_inside_item_name(writer)
156            && !writer.is_inside("span")
157            && !writer.is_inside("code")
158        {
159            writer.push_str(&format!("`{text}`"));
160            return HandlerOutcome::Handled;
161        }
162
163        HandlerOutcome::NoOp
164    }
165}
166
167pub struct RustdocChromeRemover;
168
169impl HandleTag for RustdocChromeRemover {
170    fn should_handle(&self, tag: &str) -> bool {
171        match tag {
172            "head" | "script" | "nav" | "summary" | "button" | "div" | "span" => true,
173            _ => false,
174        }
175    }
176
177    fn handle_tag_start(
178        &mut self,
179        tag: &HtmlElement,
180        _writer: &mut MarkdownWriter,
181    ) -> StartTagOutcome {
182        match tag.tag.as_str() {
183            "head" | "script" | "nav" => return StartTagOutcome::Skip,
184            "summary" => {
185                if tag.has_class("hideme") {
186                    return StartTagOutcome::Skip;
187                }
188            }
189            "button" => {
190                if tag.attr("id").as_deref() == Some("copy-path") {
191                    return StartTagOutcome::Skip;
192                }
193            }
194            "div" | "span" => {
195                let classes_to_skip = ["nav-container", "sidebar-elems", "out-of-band"];
196                if tag.has_any_classes(&classes_to_skip) {
197                    return StartTagOutcome::Skip;
198                }
199            }
200            _ => {}
201        }
202
203        StartTagOutcome::Continue
204    }
205}