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
 99pub struct RustdocTableHandler {
100    /// The number of columns in the current `<table>`.
101    current_table_columns: usize,
102    is_first_th: bool,
103    is_first_td: bool,
104}
105
106impl RustdocTableHandler {
107    pub fn new() -> Self {
108        Self {
109            current_table_columns: 0,
110            is_first_th: true,
111            is_first_td: true,
112        }
113    }
114}
115
116impl HandleTag for RustdocTableHandler {
117    fn should_handle(&self, tag: &str) -> bool {
118        match tag {
119            "table" | "thead" | "tbody" | "tr" | "th" | "td" => true,
120            _ => false,
121        }
122    }
123
124    fn handle_tag_start(
125        &mut self,
126        tag: &HtmlElement,
127        writer: &mut MarkdownWriter,
128    ) -> StartTagOutcome {
129        match tag.tag.as_str() {
130            "thead" => writer.push_blank_line(),
131            "tr" => writer.push_newline(),
132            "th" => {
133                self.current_table_columns += 1;
134                if self.is_first_th {
135                    self.is_first_th = false;
136                } else {
137                    writer.push_str(" ");
138                }
139                writer.push_str("| ");
140            }
141            "td" => {
142                if self.is_first_td {
143                    self.is_first_td = false;
144                } else {
145                    writer.push_str(" ");
146                }
147                writer.push_str("| ");
148            }
149            _ => {}
150        }
151
152        StartTagOutcome::Continue
153    }
154
155    fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
156        match tag.tag.as_str() {
157            "thead" => {
158                writer.push_newline();
159                for ix in 0..self.current_table_columns {
160                    if ix > 0 {
161                        writer.push_str(" ");
162                    }
163                    writer.push_str("| ---");
164                }
165                writer.push_str(" |");
166                self.is_first_th = true;
167            }
168            "tr" => {
169                writer.push_str(" |");
170                self.is_first_td = true;
171            }
172            "table" => {
173                self.current_table_columns = 0;
174            }
175            _ => {}
176        }
177    }
178}
179
180const RUSTDOC_ITEM_NAME_CLASS: &str = "item-name";
181
182pub struct RustdocItemHandler;
183
184impl RustdocItemHandler {
185    /// Returns whether we're currently inside of an `.item-name` element, which
186    /// rustdoc uses to display Rust items in a list.
187    fn is_inside_item_name(writer: &MarkdownWriter) -> bool {
188        writer
189            .current_element_stack()
190            .iter()
191            .any(|element| element.has_class(RUSTDOC_ITEM_NAME_CLASS))
192    }
193}
194
195impl HandleTag for RustdocItemHandler {
196    fn should_handle(&self, tag: &str) -> bool {
197        match tag {
198            "div" | "span" => true,
199            _ => false,
200        }
201    }
202
203    fn handle_tag_start(
204        &mut self,
205        tag: &HtmlElement,
206        writer: &mut MarkdownWriter,
207    ) -> StartTagOutcome {
208        match tag.tag.as_str() {
209            "div" | "span" => {
210                if Self::is_inside_item_name(writer) && tag.has_class("stab") {
211                    writer.push_str(" [");
212                }
213            }
214            _ => {}
215        }
216
217        StartTagOutcome::Continue
218    }
219
220    fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
221        match tag.tag.as_str() {
222            "div" | "span" => {
223                if tag.has_class(RUSTDOC_ITEM_NAME_CLASS) {
224                    writer.push_str(": ");
225                }
226
227                if Self::is_inside_item_name(writer) && tag.has_class("stab") {
228                    writer.push_str("]");
229                }
230            }
231            _ => {}
232        }
233    }
234
235    fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
236        if Self::is_inside_item_name(writer)
237            && !writer.is_inside("span")
238            && !writer.is_inside("code")
239        {
240            writer.push_str(&format!("`{text}`"));
241            return HandlerOutcome::Handled;
242        }
243
244        HandlerOutcome::NoOp
245    }
246}
247
248pub struct RustdocChromeRemover;
249
250impl HandleTag for RustdocChromeRemover {
251    fn should_handle(&self, tag: &str) -> bool {
252        match tag {
253            "head" | "script" | "nav" | "summary" | "button" | "div" | "span" => true,
254            _ => false,
255        }
256    }
257
258    fn handle_tag_start(
259        &mut self,
260        tag: &HtmlElement,
261        _writer: &mut MarkdownWriter,
262    ) -> StartTagOutcome {
263        match tag.tag.as_str() {
264            "head" | "script" | "nav" => return StartTagOutcome::Skip,
265            "summary" => {
266                if tag.has_class("hideme") {
267                    return StartTagOutcome::Skip;
268                }
269            }
270            "button" => {
271                if tag.attr("id").as_deref() == Some("copy-path") {
272                    return StartTagOutcome::Skip;
273                }
274            }
275            "div" | "span" => {
276                let classes_to_skip = ["nav-container", "sidebar-elems", "out-of-band"];
277                if tag.has_any_classes(&classes_to_skip) {
278                    return StartTagOutcome::Skip;
279                }
280            }
281            _ => {}
282        }
283
284        StartTagOutcome::Continue
285    }
286}