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}