sync_readme/content/rustdoc/
intra_link.rs

1use 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 item
286        local_html_root_url
287    } else {
288        // external item
289        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::ExternCrate, [..]) => todo!(),
303        // (ItemKind::Import, [..]) => todo!(),
304        (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::OpaqueTy, [..]) => todo!(),
316        (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::TraitAlias, [..]) => todo!(),
319        // (ItemKind::Impl, [..]) => todo!(),
320        (ItemKind::Static, [ps @ .., name]) => join(ps, format_args!("static.{name}.html")),
321        // (ItemKind::ForeignType, [..]) => todo!(),
322        (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        // (ItemKind::Keyword, [..]) => todo!(),
333        (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    // workaround for https://github.com/rust-lang/rust/issues/101687
350    // if the item is not found in the paths, try to find it in the extra_paths
351
352    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}