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}