rustdoc.rs

  1use indexmap::IndexMap;
  2use strum::{EnumIter, IntoEnumIterator};
  3
  4use crate::html_element::HtmlElement;
  5use crate::markdown_writer::{HandleTag, HandlerOutcome, MarkdownWriter, StartTagOutcome};
  6
  7pub struct RustdocHeadingHandler;
  8
  9impl HandleTag for RustdocHeadingHandler {
 10    fn should_handle(&self, _tag: &str) -> bool {
 11        // We're only handling text, so we don't need to visit any tags.
 12        false
 13    }
 14
 15    fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
 16        if writer.is_inside("h1")
 17            || writer.is_inside("h2")
 18            || writer.is_inside("h3")
 19            || writer.is_inside("h4")
 20            || writer.is_inside("h5")
 21            || writer.is_inside("h6")
 22        {
 23            let text = text
 24                .trim_matches(|char| char == '\n' || char == '\r' || char == 'ยง')
 25                .replace('\n', " ");
 26            writer.push_str(&text);
 27
 28            return HandlerOutcome::Handled;
 29        }
 30
 31        HandlerOutcome::NoOp
 32    }
 33}
 34
 35pub struct RustdocCodeHandler;
 36
 37impl HandleTag for RustdocCodeHandler {
 38    fn should_handle(&self, tag: &str) -> bool {
 39        match tag {
 40            "pre" | "code" => true,
 41            _ => false,
 42        }
 43    }
 44
 45    fn handle_tag_start(
 46        &mut self,
 47        tag: &HtmlElement,
 48        writer: &mut MarkdownWriter,
 49    ) -> StartTagOutcome {
 50        match tag.tag.as_str() {
 51            "code" => {
 52                if !writer.is_inside("pre") {
 53                    writer.push_str("`");
 54                }
 55            }
 56            "pre" => {
 57                let classes = tag.classes();
 58                let is_rust = classes.iter().any(|class| class == "rust");
 59                let language = is_rust
 60                    .then(|| "rs")
 61                    .or_else(|| {
 62                        classes.iter().find_map(|class| {
 63                            if let Some((_, language)) = class.split_once("language-") {
 64                                Some(language.trim())
 65                            } else {
 66                                None
 67                            }
 68                        })
 69                    })
 70                    .unwrap_or("");
 71
 72                writer.push_str(&format!("\n\n```{language}\n"));
 73            }
 74            _ => {}
 75        }
 76
 77        StartTagOutcome::Continue
 78    }
 79
 80    fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
 81        match tag.tag.as_str() {
 82            "code" => {
 83                if !writer.is_inside("pre") {
 84                    writer.push_str("`");
 85                }
 86            }
 87            "pre" => writer.push_str("\n```\n"),
 88            _ => {}
 89        }
 90    }
 91
 92    fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
 93        if writer.is_inside("pre") {
 94            writer.push_str(&text);
 95            return HandlerOutcome::Handled;
 96        }
 97
 98        HandlerOutcome::NoOp
 99    }
100}
101
102const RUSTDOC_ITEM_NAME_CLASS: &str = "item-name";
103
104pub struct RustdocItemHandler;
105
106impl RustdocItemHandler {
107    /// Returns whether we're currently inside of an `.item-name` element, which
108    /// rustdoc uses to display Rust items in a list.
109    fn is_inside_item_name(writer: &MarkdownWriter) -> bool {
110        writer
111            .current_element_stack()
112            .iter()
113            .any(|element| element.has_class(RUSTDOC_ITEM_NAME_CLASS))
114    }
115}
116
117impl HandleTag for RustdocItemHandler {
118    fn should_handle(&self, tag: &str) -> bool {
119        match tag {
120            "div" | "span" => true,
121            _ => false,
122        }
123    }
124
125    fn handle_tag_start(
126        &mut self,
127        tag: &HtmlElement,
128        writer: &mut MarkdownWriter,
129    ) -> StartTagOutcome {
130        match tag.tag.as_str() {
131            "div" | "span" => {
132                if Self::is_inside_item_name(writer) && tag.has_class("stab") {
133                    writer.push_str(" [");
134                }
135            }
136            _ => {}
137        }
138
139        StartTagOutcome::Continue
140    }
141
142    fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
143        match tag.tag.as_str() {
144            "div" | "span" => {
145                if tag.has_class(RUSTDOC_ITEM_NAME_CLASS) {
146                    writer.push_str(": ");
147                }
148
149                if Self::is_inside_item_name(writer) && tag.has_class("stab") {
150                    writer.push_str("]");
151                }
152            }
153            _ => {}
154        }
155    }
156
157    fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
158        if Self::is_inside_item_name(writer)
159            && !writer.is_inside("span")
160            && !writer.is_inside("code")
161        {
162            writer.push_str(&format!("`{text}`"));
163            return HandlerOutcome::Handled;
164        }
165
166        HandlerOutcome::NoOp
167    }
168}
169
170pub struct RustdocChromeRemover;
171
172impl HandleTag for RustdocChromeRemover {
173    fn should_handle(&self, tag: &str) -> bool {
174        match tag {
175            "head" | "script" | "nav" | "summary" | "button" | "div" | "span" => true,
176            _ => false,
177        }
178    }
179
180    fn handle_tag_start(
181        &mut self,
182        tag: &HtmlElement,
183        _writer: &mut MarkdownWriter,
184    ) -> StartTagOutcome {
185        match tag.tag.as_str() {
186            "head" | "script" | "nav" => return StartTagOutcome::Skip,
187            "summary" => {
188                if tag.has_class("hideme") {
189                    return StartTagOutcome::Skip;
190                }
191            }
192            "button" => {
193                if tag.attr("id").as_deref() == Some("copy-path") {
194                    return StartTagOutcome::Skip;
195                }
196            }
197            "div" | "span" => {
198                let classes_to_skip = ["nav-container", "sidebar-elems", "out-of-band"];
199                if tag.has_any_classes(&classes_to_skip) {
200                    return StartTagOutcome::Skip;
201                }
202            }
203            _ => {}
204        }
205
206        StartTagOutcome::Continue
207    }
208}
209
210#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
211pub enum RustdocItemKind {
212    Mod,
213    Macro,
214    Struct,
215    Enum,
216    Constant,
217    Trait,
218    Function,
219    TypeAlias,
220    AttributeMacro,
221    DeriveMacro,
222}
223
224impl RustdocItemKind {
225    const fn class(&self) -> &'static str {
226        match self {
227            Self::Mod => "mod",
228            Self::Macro => "macro",
229            Self::Struct => "struct",
230            Self::Enum => "enum",
231            Self::Constant => "constant",
232            Self::Trait => "trait",
233            Self::Function => "fn",
234            Self::TypeAlias => "type",
235            Self::AttributeMacro => "attr",
236            Self::DeriveMacro => "derive",
237        }
238    }
239}
240
241#[derive(Debug, Clone)]
242pub struct RustdocItem {
243    pub kind: RustdocItemKind,
244    pub name: String,
245}
246
247impl RustdocItem {
248    pub fn url_path(&self) -> String {
249        let name = &self.name;
250        match self.kind {
251            RustdocItemKind::Mod => format!("{name}/index.html"),
252            RustdocItemKind::Macro
253            | RustdocItemKind::Struct
254            | RustdocItemKind::Enum
255            | RustdocItemKind::Constant
256            | RustdocItemKind::Trait
257            | RustdocItemKind::Function
258            | RustdocItemKind::TypeAlias
259            | RustdocItemKind::AttributeMacro
260            | RustdocItemKind::DeriveMacro => {
261                format!("{kind}.{name}.html", kind = self.kind.class())
262            }
263        }
264    }
265}
266
267pub struct RustdocItemCollector {
268    pub items: IndexMap<(RustdocItemKind, String), RustdocItem>,
269}
270
271impl RustdocItemCollector {
272    pub fn new() -> Self {
273        Self {
274            items: IndexMap::new(),
275        }
276    }
277
278    fn parse_item(tag: &HtmlElement) -> Option<RustdocItem> {
279        if tag.tag.as_str() != "a" {
280            return None;
281        }
282
283        let href = tag.attr("href")?;
284        if href == "#" {
285            return None;
286        }
287
288        for kind in RustdocItemKind::iter() {
289            if tag.has_class(kind.class()) {
290                let name = href
291                    .trim_start_matches(&format!("{}.", kind.class()))
292                    .trim_end_matches("/index.html")
293                    .trim_end_matches(".html");
294
295                return Some(RustdocItem {
296                    kind,
297                    name: name.to_owned(),
298                });
299            }
300        }
301
302        None
303    }
304}
305
306impl HandleTag for RustdocItemCollector {
307    fn should_handle(&self, tag: &str) -> bool {
308        tag == "a"
309    }
310
311    fn handle_tag_start(
312        &mut self,
313        tag: &HtmlElement,
314        writer: &mut MarkdownWriter,
315    ) -> StartTagOutcome {
316        match tag.tag.as_str() {
317            "a" => {
318                let is_reexport = writer.current_element_stack().iter().any(|element| {
319                    if let Some(id) = element.attr("id") {
320                        id.starts_with("reexport.")
321                    } else {
322                        false
323                    }
324                });
325
326                if !is_reexport {
327                    if let Some(item) = Self::parse_item(tag) {
328                        self.items.insert((item.kind, item.name.clone()), item);
329                    }
330                }
331            }
332            _ => {}
333        }
334
335        StartTagOutcome::Continue
336    }
337}