sync_readme/content/rustdoc/
intra_link.rs1use std::borrow::Cow;
2use std::cmp::Reverse;
3use std::collections::{BTreeMap, BinaryHeap, HashMap};
4use std::fmt::Write;
5use std::hash::BuildHasher;
6use std::rc::Rc;
7
8use pulldown_cmark::{BrokenLink, CowStr, Event, Options, Tag};
9use rustdoc_types::{Crate, Id, Item, ItemEnum, ItemKind, ItemSummary, MacroKind, StructKind, VariantKind};
10
11trait CowStrExt<'a> {
12 fn as_str(&'a self) -> &'a str;
13}
14
15impl<'a> CowStrExt<'a> for CowStr<'a> {
16 fn as_str(&'a self) -> &'a str {
17 match self {
18 CowStr::Boxed(s) => s,
19 CowStr::Borrowed(s) => s,
20 CowStr::Inlined(s) => s,
21 }
22 }
23}
24
25#[derive(Debug)]
26pub(super) struct Parser<B, M> {
27 broken_link_callback: B,
28 iterator_map: M,
29}
30
31type BrokenLinkPair<'a> = (CowStr<'a>, CowStr<'a>);
32
33impl Parser<(), ()> {
34 pub(super) fn new<'a>(
35 doc: &'a Crate,
36 item: &'a Item,
37 local_html_root_url: &str,
38 mappings: &BTreeMap<String, String>,
39 ) -> Parser<impl FnMut(BrokenLink<'_>) -> Option<BrokenLinkPair<'a>>, impl FnMut(Event<'a>) -> Option<Event<'a>>> {
40 let url_map = Rc::new(resolve_links(doc, item, local_html_root_url, mappings));
41
42 let broken_link_callback = {
43 let url_map = Rc::clone(&url_map);
44 move |link: BrokenLink<'_>| {
45 let url = url_map.get(link.reference.as_str())?.as_ref()?;
46 Some((url.to_owned().into(), "".into()))
47 }
48 };
49 let iterator_map = move |event| convert_link(&url_map, event);
50
51 Parser {
52 broken_link_callback,
53 iterator_map,
54 }
55 }
56}
57
58impl<'a, B, M> Parser<B, M>
59where
60 B: FnMut(BrokenLink<'_>) -> Option<BrokenLinkPair<'a>> + 'a,
61 M: FnMut(Event<'a>) -> Option<Event<'a>> + 'a,
62{
63 pub(super) fn events<'b>(&'b mut self, doc: &'a str) -> impl Iterator<Item = Event<'a>> + 'b
64 where
65 'a: 'b,
66 {
67 pulldown_cmark::Parser::new_with_broken_link_callback(doc, Options::all(), Some(&mut self.broken_link_callback))
68 .filter_map(&mut self.iterator_map)
69 }
70}
71
72fn resolve_links<'doc>(
73 doc: &'doc Crate,
74 item: &'doc Item,
75 local_html_root_url: &str,
76 mappings: &BTreeMap<String, String>,
77) -> BTreeMap<&'doc str, Option<String>> {
78 let extra_paths = extra_paths(&doc.index, &doc.paths);
79 item.links
80 .iter()
81 .map(move |(name, id)| {
82 if let Some(path) = mappings.get(name) {
83 (name.as_str(), Some(path.clone()))
84 } else {
85 let url = id_to_url(doc, &extra_paths, local_html_root_url, id).or_else(|| {
86 eprintln!("failed to resolve link to `{name}`; id={id:?}");
87 None
88 });
89 (name.as_str(), url)
90 }
91 })
92 .collect()
93}
94
95#[derive(Debug)]
96struct Node<'a> {
97 depth: usize,
98 kind: ItemKind,
99 name: Option<&'a str>,
100 parent: Option<&'a Id>,
101}
102
103fn extra_paths<'doc, S: BuildHasher + Default>(
104 index: &'doc HashMap<Id, Item, S>,
105 paths: &'doc HashMap<Id, ItemSummary, S>,
106) -> HashMap<&'doc Id, Node<'doc>, S> {
107 let mut map: HashMap<&Id, Node<'_>, S> = index
108 .iter()
109 .map(|(id, item)| {
110 (
111 id,
112 Node {
113 depth: usize::MAX,
114 kind: item_kind(item),
115 name: item.name.as_deref(),
116 parent: None,
117 },
118 )
119 })
120 .collect();
121
122 #[derive(Debug)]
123 struct HeapItem<'doc> {
124 depth: Reverse<usize>,
125 id: &'doc Id,
126 parent: Option<&'doc Id>,
127 item: &'doc Item,
128 }
129
130 impl PartialOrd for HeapItem<'_> {
131 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
132 Some(self.cmp(other))
133 }
134 }
135 impl Ord for HeapItem<'_> {
136 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
137 self.depth.cmp(&other.depth)
138 }
139 }
140 impl PartialEq for HeapItem<'_> {
141 fn eq(&self, other: &Self) -> bool {
142 self.depth == other.depth
143 }
144 }
145 impl Eq for HeapItem<'_> {}
146
147 let mut heap: BinaryHeap<HeapItem<'_>> = index
148 .iter()
149 .map(|(id, item)| {
150 let depth = if paths.contains_key(id) { 0 } else { usize::MAX };
151 HeapItem {
152 depth: Reverse(depth),
153 id,
154 item,
155 parent: None,
156 }
157 })
158 .collect();
159
160 while let Some(HeapItem {
161 depth: Reverse(depth),
162 id,
163 parent,
164 item,
165 }) = heap.pop()
166 {
167 let node = map.get_mut(id).unwrap();
168 if depth >= node.depth {
169 continue;
170 }
171 node.parent = parent;
172
173 map.get_mut(id).unwrap().depth = depth;
174
175 for child in item_children(item).into_iter().flatten() {
176 let child = match index.get(child) {
177 Some(child) => child,
178 None => {
179 continue;
180 }
181 };
182 let child_depth = depth + 1;
183 heap.push(HeapItem {
184 depth: Reverse(child_depth),
185 id: &child.id,
186 item: child,
187 parent: Some(id),
188 });
189 }
190 }
191
192 map
193}
194
195fn item_kind(item: &Item) -> ItemKind {
196 match &item.inner {
197 ItemEnum::Module(_) => ItemKind::Module,
198 ItemEnum::ExternCrate { .. } => ItemKind::ExternCrate,
199 ItemEnum::Use(_) => ItemKind::Use,
200 ItemEnum::Union(_) => ItemKind::Union,
201 ItemEnum::Struct(_) => ItemKind::Struct,
202 ItemEnum::StructField(_) => ItemKind::StructField,
203 ItemEnum::Enum(_) => ItemKind::Enum,
204 ItemEnum::Variant(_) => ItemKind::Variant,
205 ItemEnum::Function(_) => ItemKind::Function,
206 ItemEnum::Trait(_) => ItemKind::Trait,
207 ItemEnum::TraitAlias(_) => ItemKind::TraitAlias,
208 ItemEnum::Impl(_) => ItemKind::Impl,
209 ItemEnum::TypeAlias(_) => ItemKind::TypeAlias,
210 ItemEnum::Constant { .. } => ItemKind::Constant,
211 ItemEnum::Static(_) => ItemKind::Static,
212 ItemEnum::ExternType => ItemKind::ExternType,
213 ItemEnum::Macro(_) => ItemKind::Macro,
214 ItemEnum::ProcMacro(pm) => match pm.kind {
215 MacroKind::Bang => ItemKind::Macro,
216 MacroKind::Attr => ItemKind::ProcAttribute,
217 MacroKind::Derive => ItemKind::ProcDerive,
218 },
219 ItemEnum::Primitive(_) => ItemKind::Primitive,
220 ItemEnum::AssocConst { .. } => ItemKind::AssocConst,
221 ItemEnum::AssocType { .. } => ItemKind::AssocType,
222 }
223}
224
225fn item_children<'doc>(parent: &'doc Item) -> Option<Box<dyn Iterator<Item = &'doc Id> + 'doc>> {
226 match &parent.inner {
227 ItemEnum::Module(m) => Some(Box::new(m.items.iter())),
228 ItemEnum::ExternCrate { .. } => None,
229 ItemEnum::Use(_) => None,
230 ItemEnum::Union(u) => Some(Box::new(u.fields.iter())),
231 ItemEnum::Struct(s) => match &s.kind {
232 StructKind::Unit => None,
233 StructKind::Tuple(t) => Some(Box::new(t.iter().flatten())),
234 StructKind::Plain {
235 fields,
236 has_stripped_fields: _,
237 } => Some(Box::new(fields.iter())),
238 },
239 ItemEnum::StructField(_) => None,
240 ItemEnum::Enum(e) => Some(Box::new(e.variants.iter())),
241 ItemEnum::Variant(v) => match &v.kind {
242 VariantKind::Plain => None,
243 VariantKind::Tuple(t) => Some(Box::new(t.iter().flatten())),
244 VariantKind::Struct {
245 fields,
246 has_stripped_fields: _,
247 } => Some(Box::new(fields.iter())),
248 },
249 ItemEnum::Function(_) => None,
250 ItemEnum::Trait(t) => Some(Box::new(t.items.iter())),
251 ItemEnum::TraitAlias(_) => None,
252 ItemEnum::Impl(i) => Some(Box::new(i.items.iter())),
253 ItemEnum::TypeAlias(_) => None,
254 ItemEnum::Constant { .. } => None,
255 ItemEnum::Static(_) => None,
256 ItemEnum::ExternType => None,
257 ItemEnum::Macro(_) => None,
258 ItemEnum::ProcMacro(_) => None,
259 ItemEnum::Primitive(_) => None,
260 ItemEnum::AssocConst { .. } => None,
261 ItemEnum::AssocType { .. } => None,
262 }
263}
264
265fn convert_link<'a>(url_map: &BTreeMap<&str, Option<String>>, mut event: Event<'a>) -> Option<Event<'a>> {
266 if let Event::Start(Tag::Link { dest_url: url, .. }) = &mut event
267 && let Some(full_url) = url_map.get(url.as_ref())
268 {
269 match full_url {
270 Some(full_url) => *url = full_url.to_owned().into(),
271 None => return None,
272 }
273 }
274 Some(event)
275}
276
277fn id_to_url<S: BuildHasher + Default>(
278 doc: &Crate,
279 extra_paths: &HashMap<&Id, Node<'_>, S>,
280 local_html_root_url: &str,
281 id: &Id,
282) -> Option<String> {
283 let item = item_summary(doc, extra_paths, id)?;
284 let html_root_url = if item.crate_id == 0 {
285 local_html_root_url
287 } else {
288 let external_crate = doc.external_crates.get(&item.crate_id)?;
290 external_crate.html_root_url.as_ref()?
291 };
292
293 let mut url = html_root_url.trim_end_matches('/').to_owned();
294 let mut join = |paths: &[String], args| {
295 for path in paths {
296 write!(&mut url, "/{path}").unwrap();
297 }
298 write!(&mut url, "/{args}").unwrap();
299 };
300 match (&item.kind, item.path.as_slice()) {
301 (ItemKind::Module, ps) => join(ps, format_args!("index.html")),
302 (ItemKind::Struct, [ps @ .., name]) => join(ps, format_args!("struct.{name}.html")),
305 (ItemKind::StructField, [ps @ .., struct_name, field]) => {
306 join(ps, format_args!("struct.{struct_name}.html#structfield.{field}"))
307 }
308 (ItemKind::Union, [ps @ .., name]) => join(ps, format_args!("union.{name}.html")),
309 (ItemKind::Enum, [ps @ .., name]) => join(ps, format_args!("enum.{name}.html")),
310 (ItemKind::Variant, [ps @ .., enum_name, variant_name]) => {
311 join(ps, format_args!("enum.{enum_name}.html#variant.{variant_name}"))
312 }
313 (ItemKind::Function, [ps @ .., name]) => join(ps, format_args!("fn.{name}.html")),
314 (ItemKind::TypeAlias, [ps @ .., name]) => join(ps, format_args!("type.{name}.html")),
315 (ItemKind::Constant, [ps @ .., name]) => join(ps, format_args!("constant.{name}.html")),
317 (ItemKind::Trait, [ps @ .., name]) => join(ps, format_args!("trait.{name}.html")),
318 (ItemKind::Static, [ps @ .., name]) => join(ps, format_args!("static.{name}.html")),
321 (ItemKind::Macro, [ps @ .., name]) => join(ps, format_args!("macro.{name}.html")),
323 (ItemKind::ProcAttribute, [ps @ .., name]) => join(ps, format_args!("attr.{name}.html")),
324 (ItemKind::ProcDerive, [ps @ .., name]) => join(ps, format_args!("derive.{name}.html")),
325 (ItemKind::AssocConst, [ps @ .., trait_name, const_name]) => {
326 join(ps, format_args!("trait.{trait_name}.html#associatedconstant.{const_name}"))
327 }
328 (ItemKind::AssocType, [ps @ .., trait_name, type_name]) => {
329 join(ps, format_args!("trait.{trait_name}.html#associatedtype.{type_name}"))
330 }
331 (ItemKind::Primitive, [ps @ .., name]) => join(ps, format_args!("primitive.{name}.html")),
332 (item, path) => {
334 eprintln!("unexpected intra-doc link item & path found; path={path:?}, item={item:?}");
335 return None;
336 }
337 }
338 Some(url)
339}
340
341fn item_summary<'doc, S: BuildHasher + Default>(
342 doc: &'doc Crate,
343 extra_paths: &'doc HashMap<&'doc Id, Node<'doc>, S>,
344 id: &'doc Id,
345) -> Option<Cow<'doc, ItemSummary>> {
346 if let Some(summary) = doc.paths.get(id) {
347 return Some(Cow::Borrowed(summary));
348 }
349 let node = extra_paths.get(id)?;
353 let mut stack = vec![node];
354 let mut current = node;
355 while let Some(parent) = current.parent {
356 if let Some(summary) = doc.paths.get(parent) {
357 let mut path = summary.path.clone();
358 while let Some(node) = stack.pop() {
359 let name = node.name?;
360 path.push(name.to_string());
361 }
362 return Some(Cow::Owned(ItemSummary {
363 crate_id: summary.crate_id,
364 kind: node.kind,
365 path,
366 }));
367 }
368 current = extra_paths.get(&parent)?;
369 stack.push(current);
370 }
371 None
372}