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}