cargo_rdme/transform/intralinks/
mod.rs

1use crate::Doc;
2use crate::transform::DocTransform;
3use crate::transform::intralinks::links::{
4    Link, MarkdownLink, markdown_link_iterator, markdown_reference_link_definition_iterator,
5};
6use module_walker::walk_module_file;
7use std::collections::{HashMap, HashSet};
8use std::fmt;
9use std::fmt::Write;
10use std::hash::{Hash, Hasher};
11use std::path::{Path, PathBuf};
12use std::rc::Rc;
13use syn::{Item, ItemMod};
14use thiserror::Error;
15use unicase::UniCase;
16
17mod links;
18mod module_walker;
19
20#[derive(Error, Debug)]
21pub enum IntralinkError {
22    #[error("IO error: {0}")]
23    IOError(std::io::Error),
24    #[error("failed to analyzing code: {0}")]
25    AstWalkError(module_walker::ModuleWalkError),
26    #[error("failed to load standard library: {0}")]
27    LoadStdLibError(String),
28}
29
30impl From<std::io::Error> for IntralinkError {
31    fn from(err: std::io::Error) -> Self {
32        IntralinkError::IOError(err)
33    }
34}
35
36impl From<module_walker::ModuleWalkError> for IntralinkError {
37    fn from(err: module_walker::ModuleWalkError) -> Self {
38        IntralinkError::AstWalkError(err)
39    }
40}
41
42#[derive(Default, Debug, PartialEq, Eq, Clone)]
43pub struct IntralinksDocsRsConfig {
44    pub docs_rs_base_url: Option<String>,
45    pub docs_rs_version: Option<String>,
46}
47
48#[derive(Default, Debug, PartialEq, Eq, Clone)]
49pub struct IntralinksConfig {
50    pub docs_rs: IntralinksDocsRsConfig,
51    pub strip_links: Option<bool>,
52}
53
54pub struct DocTransformIntralinks<F> {
55    crate_name: String,
56    entrypoint: PathBuf,
57    emit_warning: F,
58    config: IntralinksConfig,
59}
60
61impl<F> DocTransformIntralinks<F>
62where
63    F: Fn(&str),
64{
65    pub fn new(
66        crate_name: impl Into<String>,
67        entrypoint: impl AsRef<Path>,
68        emit_warning: F,
69        config: Option<IntralinksConfig>,
70    ) -> DocTransformIntralinks<F> {
71        DocTransformIntralinks {
72            crate_name: crate_name.into(),
73            entrypoint: entrypoint.as_ref().to_path_buf(),
74            emit_warning,
75            config: config.unwrap_or_default(),
76        }
77    }
78}
79
80impl<F> DocTransform for DocTransformIntralinks<F>
81where
82    F: Fn(&str),
83{
84    type E = IntralinkError;
85
86    fn transform(&self, doc: &Doc) -> Result<Doc, IntralinkError> {
87        let symbols: HashSet<ItemPath> = extract_markdown_intralink_symbols(doc);
88
89        // If there are no intralinks in the doc don't even bother doing anything else.
90        if symbols.is_empty() {
91            return Ok(doc.clone());
92        }
93
94        // We only load symbols type information when we need them.
95        let symbols_type = match self.config.strip_links.unwrap_or(false) {
96            false => load_symbols_type(&self.entrypoint, &symbols, &self.emit_warning)?,
97            true => HashMap::new(),
98        };
99
100        let doc =
101            rewrite_links(doc, &symbols_type, &self.crate_name, &self.emit_warning, &self.config);
102
103        Ok(doc)
104    }
105}
106
107fn rewrite_links(
108    doc: &Doc,
109    symbols_type: &HashMap<ItemPath, SymbolType>,
110    crate_name: &str,
111    emit_warning: &impl Fn(&str),
112    config: &IntralinksConfig,
113) -> Doc {
114    let RewriteReferenceLinksResult { doc, reference_links_to_remove } =
115        rewrite_reference_links_definitions(doc, symbols_type, crate_name, emit_warning, config);
116
117    // TODO Refactor link removal code so that it all happens in a new phase and not inside the
118    //      functions above.
119    rewrite_markdown_links(
120        &doc,
121        symbols_type,
122        crate_name,
123        emit_warning,
124        config,
125        &reference_links_to_remove,
126    )
127}
128
129#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
130pub enum ItemPathAnchor {
131    /// The anchor of a path starting with `::` such as `::std::fs::read`.
132    Root,
133    /// The anchor of a path starting with `crate` such as `crate::foo::is_prime`.
134    Crate,
135}
136
137/// The rust path of an item, such as `foo::bar::is_prime` or `crate`.
138#[derive(Clone)]
139pub struct ItemPath {
140    pub anchor: ItemPathAnchor,
141
142    /// This path vector can be shared and can end after the `item_path` we are representing.
143    /// This allows us to have a faster implementation for `ItemPath::all_ancestors()`.
144    path_shared: Rc<Vec<String>>,
145    path_end: usize,
146}
147
148impl ItemPath {
149    fn new(anchor: ItemPathAnchor) -> ItemPath {
150        ItemPath { anchor, path_shared: Rc::new(Vec::new()), path_end: 0 }
151    }
152
153    fn root(crate_name: &str) -> ItemPath {
154        ItemPath::new(ItemPathAnchor::Root).join(&crate_name)
155    }
156
157    fn from_string(s: &str) -> Option<ItemPath> {
158        let anchor;
159        let rest;
160
161        if let Some(r) = s.strip_prefix("::") {
162            anchor = ItemPathAnchor::Root;
163            rest = r;
164        } else if s == "crate" {
165            return Some(ItemPath::new(ItemPathAnchor::Crate));
166        } else if let Some(r) = s.strip_prefix("crate::") {
167            anchor = ItemPathAnchor::Crate;
168            rest = r;
169        } else {
170            return None;
171        }
172
173        if rest.is_empty() {
174            return None;
175        }
176
177        let path: Rc<Vec<String>> = Rc::new(rest.split("::").map(str::to_owned).collect());
178
179        Some(ItemPath { anchor, path_end: path.len(), path_shared: path })
180    }
181
182    fn path_components(&self) -> impl Iterator<Item = &str> {
183        self.path_shared[0..self.path_end].iter().map(String::as_str)
184    }
185
186    fn is_toplevel(&self) -> bool {
187        match self.anchor {
188            ItemPathAnchor::Root => self.path_end <= 1,
189            ItemPathAnchor::Crate => self.path_end == 0,
190        }
191    }
192
193    fn parent(mut self) -> Option<ItemPath> {
194        match self.is_toplevel() {
195            true => None,
196            false => {
197                self.path_end -= 1;
198                Some(self)
199            }
200        }
201    }
202
203    fn name(&self) -> Option<&str> {
204        self.path_end.checked_sub(1).and_then(|i| self.path_shared.get(i)).map(String::as_str)
205    }
206
207    fn join(mut self, s: &impl ToString) -> ItemPath {
208        let path = Rc::make_mut(&mut self.path_shared);
209        path.truncate(self.path_end);
210        path.push(s.to_string());
211        self.path_end += 1;
212        self
213    }
214
215    fn all_ancestors(&self) -> impl Iterator<Item = ItemPath> + use<> {
216        let first_ancestor = self.clone().parent();
217
218        std::iter::successors(first_ancestor, |ancestor| ancestor.clone().parent())
219    }
220}
221
222impl PartialEq for ItemPath {
223    fn eq(&self, other: &Self) -> bool {
224        self.anchor == other.anchor && self.path_components().eq(other.path_components())
225    }
226}
227
228impl Eq for ItemPath {}
229
230impl Hash for ItemPath {
231    fn hash<H: Hasher>(&self, state: &mut H) {
232        self.anchor.hash(state);
233        self.path_components().for_each(|c| c.hash(state));
234    }
235}
236
237impl fmt::Display for ItemPath {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
239        match self.anchor {
240            ItemPathAnchor::Root => (),
241            ItemPathAnchor::Crate => f.write_str("crate")?,
242        }
243
244        for s in self.path_components() {
245            f.write_str("::")?;
246            f.write_str(s)?;
247        }
248
249        Ok(())
250    }
251}
252
253impl fmt::Debug for ItemPath {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
255        fmt::Display::fmt(self, f)
256    }
257}
258
259#[derive(Copy, Clone, Debug, PartialEq, Eq)]
260pub enum ImplSymbolType {
261    Method,
262    Const,
263    Type,
264}
265
266#[derive(Copy, Clone, Debug, PartialEq, Eq)]
267pub enum SymbolType {
268    Crate,
269    Struct,
270    Trait,
271    Enum,
272    Union,
273    Type,
274    Mod,
275    Macro,
276    Const,
277    Fn,
278    Static,
279    ImplItem(ImplSymbolType),
280}
281
282impl SymbolType {
283    /// Returns the path of the module where this item is defined.
284    ///
285    /// Importantly, if inse module `crate::amod` we have a `struct Foo` with method `Foo::method()`,
286    /// this will `Foo::method()` return `crate::amod`.
287    fn get_module_path(self, path: &ItemPath) -> Option<ItemPath> {
288        match self {
289            SymbolType::Crate => {
290                assert!(path.is_toplevel(), "a crate should always be in a toplevel path");
291                None
292            }
293            SymbolType::Struct
294            | SymbolType::Trait
295            | SymbolType::Enum
296            | SymbolType::Union
297            | SymbolType::Type
298            | SymbolType::Mod
299            | SymbolType::Macro
300            | SymbolType::Const
301            | SymbolType::Fn
302            | SymbolType::Static => {
303                let p = path.clone().parent().unwrap_or_else(|| {
304                    panic!("item {path} of type {self:?} should have a parent module")
305                });
306                Some(p)
307            }
308            SymbolType::ImplItem(_) => {
309                let p = path
310                    .clone()
311                    .parent()
312                    .unwrap_or_else(|| {
313                        panic!("item {path} of type {self:?} should have a parent type")
314                    })
315                    .parent()
316                    .unwrap_or_else(|| {
317                        panic!("item {path} of type {self:?} should have a parent module")
318                    });
319                Some(p)
320            }
321        }
322    }
323}
324
325fn symbols_type_impl_block(
326    module: &ItemPath,
327    impl_block: &syn::ItemImpl,
328) -> Vec<(ItemPath, SymbolType)> {
329    use syn::{ImplItem, Type, TypePath};
330
331    if let Type::Path(TypePath { qself: None, path }) = &*impl_block.self_ty {
332        if let Some(self_ident) = path.get_ident().map(ToString::to_string) {
333            let self_path = module.clone().join(&self_ident);
334
335            return impl_block
336                .items
337                .iter()
338                .filter_map(|item| match item {
339                    ImplItem::Fn(m) => {
340                        let ident = m.sig.ident.to_string();
341
342                        Some((ident, ImplSymbolType::Method))
343                    }
344                    ImplItem::Const(c) => {
345                        let ident = c.ident.to_string();
346
347                        Some((ident, ImplSymbolType::Const))
348                    }
349                    ImplItem::Type(t) => {
350                        let ident = t.ident.to_string();
351
352                        Some((ident, ImplSymbolType::Type))
353                    }
354                    _ => None,
355                })
356                .map(|(ident, tpy)| (self_path.clone().join(&ident), SymbolType::ImplItem(tpy)))
357                .collect();
358        }
359    }
360
361    Vec::new()
362}
363
364fn item_symbols_type(module: &ItemPath, item: &Item) -> Vec<(ItemPath, SymbolType)> {
365    let item_path = |ident: &syn::Ident| module.clone().join(ident);
366
367    let (path, symbol_type) = match item {
368        Item::Enum(e) => (item_path(&e.ident), SymbolType::Enum),
369        Item::Struct(s) => (item_path(&s.ident), SymbolType::Struct),
370        Item::Trait(t) => (item_path(&t.ident), SymbolType::Trait),
371        Item::Union(u) => (item_path(&u.ident), SymbolType::Union),
372        Item::Type(t) => (item_path(&t.ident), SymbolType::Type),
373        Item::Mod(m) => (item_path(&m.ident), SymbolType::Mod),
374        Item::Macro(syn::ItemMacro { ident: Some(ident), .. }) => {
375            (item_path(ident), SymbolType::Macro)
376        }
377        Item::Const(c) => (item_path(&c.ident), SymbolType::Const),
378        Item::Fn(f) => (item_path(&f.sig.ident), SymbolType::Fn),
379        Item::Static(s) => (item_path(&s.ident), SymbolType::Static),
380        Item::Impl(impl_block) => {
381            return symbols_type_impl_block(module, impl_block);
382        }
383
384        _ => return Vec::new(),
385    };
386
387    vec![(path, symbol_type)]
388}
389
390fn is_cfg_test(attribute: &syn::Attribute) -> bool {
391    let test_attribute: syn::Attribute = syn::parse_quote!(#[cfg(test)]);
392
393    *attribute == test_attribute
394}
395
396fn visit_module_item(
397    save_symbol: impl Fn(&ItemPath) -> bool,
398    symbols_type: &mut HashMap<ItemPath, SymbolType>,
399    module: &ItemPath,
400    item: &Item,
401) {
402    for (symbol, symbol_type) in item_symbols_type(module, item) {
403        if save_symbol(&symbol) {
404            symbols_type.insert(symbol, symbol_type);
405        }
406    }
407}
408
409/// Returns whether we should explore a module.
410fn check_explore_module(
411    should_explore_module: impl Fn(&ItemPath) -> bool,
412    modules_visited: &mut HashSet<ItemPath>,
413    mod_symbol: &ItemPath,
414    mod_item: &ItemMod,
415) -> bool {
416    // Conditional compilation can create multiple module definitions, e.g.
417    //
418    // ```
419    // #[cfg(foo)]
420    // mod a {}
421    // #[cfg(not(foo))]
422    // mod a {}
423    // ```
424    //
425    // We choose to consider the first one only.
426    if modules_visited.contains(mod_symbol) {
427        return false;
428    }
429
430    // If a module is gated by `#[cfg(test)]` we skip it.  This happens sometimes in the
431    // standard library, and we want to explore the correct, non-test, module.
432    if mod_item.attrs.iter().any(is_cfg_test) {
433        return false;
434    }
435
436    let explore = should_explore_module(mod_symbol);
437
438    if explore {
439        modules_visited.insert(mod_symbol.clone());
440    }
441
442    explore
443}
444
445fn explore_crate<P: AsRef<Path>>(
446    file: P,
447    crate_symbol: &ItemPath,
448    symbols: &HashSet<ItemPath>,
449    paths_to_explore: &HashSet<ItemPath>,
450    symbols_type: &mut HashMap<ItemPath, SymbolType>,
451    emit_warning: &impl Fn(&str),
452) -> Result<(), module_walker::ModuleWalkError> {
453    let mut modules_visited: HashSet<ItemPath> = HashSet::new();
454
455    // Walking the module only visits items, which means we need to add the root `crate` explicitly.
456    symbols_type.insert(crate_symbol.clone(), SymbolType::Crate);
457
458    let mut visit = |module: &ItemPath, item: &Item| {
459        let save_symbol = |symbol: &ItemPath| {
460            // We also check if it belongs to the paths to explore because of impl items (e.g. a
461            // method `Foo::method` we need to know about `Foo` type.  For instance if `Foo` is a
462            // struct then the link will be `⋯/struct.Foo.html#method.method`.
463            symbols.contains(symbol) || paths_to_explore.contains(symbol)
464        };
465
466        visit_module_item(save_symbol, symbols_type, module, item);
467    };
468
469    let mut explore_module = |mod_symbol: &ItemPath, mod_item: &ItemMod| -> bool {
470        check_explore_module(
471            |mod_symbol| paths_to_explore.contains(mod_symbol),
472            &mut modules_visited,
473            mod_symbol,
474            mod_item,
475        )
476    };
477
478    walk_module_file(file, crate_symbol, &mut visit, &mut explore_module, emit_warning)
479}
480
481fn load_symbols_type<P: AsRef<Path>>(
482    entry_point: P,
483    symbols: &HashSet<ItemPath>,
484    emit_warning: &impl Fn(&str),
485) -> Result<HashMap<ItemPath, SymbolType>, IntralinkError> {
486    let paths_to_explore: HashSet<ItemPath> = all_ancestor_paths(symbols.iter());
487    let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
488
489    // Only load standard library information if needed.
490    let std_lib_crates = match references_standard_library(symbols) {
491        true => get_standard_libraries()?,
492        false => Vec::new(),
493    };
494
495    for Crate { name, entrypoint } in std_lib_crates {
496        explore_crate(
497            entrypoint,
498            &ItemPath::root(&name),
499            symbols,
500            &paths_to_explore,
501            &mut symbols_type,
502            emit_warning,
503        )?;
504    }
505
506    explore_crate(
507        entry_point,
508        &ItemPath::new(ItemPathAnchor::Crate),
509        symbols,
510        &paths_to_explore,
511        &mut symbols_type,
512        emit_warning,
513    )?;
514
515    Ok(symbols_type)
516}
517
518/// Create a set with all ancestor paths of `symbols`.  For instance, if `symbols` is
519/// `{crate::foo::bar::baz, crate::baz::mumble}` it will return
520/// `{crate, crate::foo, crate::foo::bar, crate::baz}`.
521fn all_ancestor_paths<'a>(symbols: impl Iterator<Item = &'a ItemPath>) -> HashSet<ItemPath> {
522    symbols.into_iter().flat_map(ItemPath::all_ancestors).collect()
523}
524
525fn extract_markdown_intralink_symbols(doc: &Doc) -> HashSet<ItemPath> {
526    let item_paths_inline_links =
527        markdown_link_iterator(&doc.markdown).items().filter_map(|l| match l {
528            MarkdownLink::Inline { link: inline_link } => inline_link.link.link_as_item_path(),
529            MarkdownLink::Reference { .. } => None,
530        });
531
532    let item_paths_reference_link_def = markdown_reference_link_definition_iterator(&doc.markdown)
533        .items()
534        .filter_map(|l| l.link.link_as_item_path());
535
536    item_paths_inline_links.chain(item_paths_reference_link_def).collect()
537}
538
539/// Returns the url for the item.
540///
541/// This returns `None` if the item type(s) was not successfully resolved.
542fn documentation_url(
543    item_path: &ItemPath,
544    symbols_type: &HashMap<ItemPath, SymbolType>,
545    crate_name: &str,
546    fragment: Option<&str>,
547    config: &IntralinksDocsRsConfig,
548) -> Option<String> {
549    let package_name = crate_name.replace('-', "_");
550    let typ = *symbols_type.get(item_path)?;
551
552    let mut link = match item_path.anchor {
553        ItemPathAnchor::Root => {
554            let std_crate_name =
555                item_path.path_components().next().expect("a root path should not be empty");
556            format!("https://doc.rust-lang.org/stable/{std_crate_name}/")
557        }
558        ItemPathAnchor::Crate => {
559            let base_url =
560                config.docs_rs_base_url.as_ref().map_or("https://docs.rs", String::as_str);
561            let version = config.docs_rs_version.as_ref().map_or("latest", String::as_str);
562
563            format!("{base_url}/{crate_name}/{version}/{package_name}/")
564        }
565    };
566
567    if typ == SymbolType::Crate {
568        return Some(format!("{}{}", link, fragment.unwrap_or("")));
569    }
570
571    let skip_components = match item_path.anchor {
572        ItemPathAnchor::Root => 1,
573        ItemPathAnchor::Crate => 0,
574    };
575
576    let module_path = typ.get_module_path(item_path).expect("item should belong to a module");
577
578    for s in module_path.path_components().skip(skip_components) {
579        link.push_str(s);
580        link.push('/');
581    }
582
583    let name =
584        item_path.name().unwrap_or_else(|| panic!("failed to get last component of {item_path}"));
585
586    match typ {
587        SymbolType::Crate => unreachable!(),
588        SymbolType::Struct => write!(&mut link, "struct.{name}.html"),
589        SymbolType::Trait => write!(&mut link, "trait.{name}.html"),
590        SymbolType::Enum => write!(&mut link, "enum.{name}.html"),
591        SymbolType::Union => write!(&mut link, "union.{name}.html"),
592        SymbolType::Type => write!(&mut link, "type.{name}.html"),
593        SymbolType::Mod => write!(&mut link, "{name}/"),
594        SymbolType::Macro => write!(&mut link, "macro.{name}.html"),
595        SymbolType::Const => write!(&mut link, "const.{name}.html"),
596        SymbolType::Fn => write!(&mut link, "fn.{name}.html"),
597        SymbolType::Static => write!(&mut link, "static.{name}.html"),
598        SymbolType::ImplItem(typ) => {
599            let parent_path = item_path
600                .clone()
601                .parent()
602                .unwrap_or_else(|| panic!("item {item_path} should always have a parent"));
603
604            let link = documentation_url(
605                &parent_path,
606                symbols_type,
607                crate_name,
608                // We discard the fragment.
609                None,
610                config,
611            )?;
612
613            let impl_item_fragment_str = match typ {
614                ImplSymbolType::Method => "method",
615                ImplSymbolType::Const => "associatedconstant",
616                ImplSymbolType::Type => "associatedtype",
617            };
618
619            return Some(format!("{link}#{impl_item_fragment_str}.{name}"));
620        }
621    }
622    .expect("this should never fail");
623
624    Some(format!("{}{}", link, fragment.unwrap_or("")))
625}
626
627enum MarkdownLinkAction {
628    Link(Link),
629    Preserve,
630    Strip,
631}
632
633fn markdown_link(
634    link: &Link,
635    symbols_type: &HashMap<ItemPath, SymbolType>,
636    crate_name: &str,
637    emit_warning: &impl Fn(&str),
638    config: &IntralinksConfig,
639) -> MarkdownLinkAction {
640    match link.link_as_item_path() {
641        Some(symbol) => {
642            let link = documentation_url(
643                &symbol,
644                symbols_type,
645                crate_name,
646                link.link_fragment(),
647                &config.docs_rs,
648            );
649
650            match link {
651                Some(l) => MarkdownLinkAction::Link(l.into()),
652                None => {
653                    emit_warning(&format!("Could not resolve definition of `{symbol}`."));
654
655                    // This was an intralink, but we were not able to generate a link.
656                    MarkdownLinkAction::Strip
657                }
658            }
659        }
660        None => MarkdownLinkAction::Preserve,
661    }
662}
663
664fn rewrite_markdown_links(
665    doc: &Doc,
666    symbols_type: &HashMap<ItemPath, SymbolType>,
667    crate_name: &str,
668    emit_warning: &impl Fn(&str),
669    config: &IntralinksConfig,
670    reference_links_to_remove: &HashSet<UniCase<String>>,
671) -> Doc {
672    use crate::utils::ItemOrOther;
673
674    let strip_links = config.strip_links.unwrap_or(false);
675    let mut new_doc = String::with_capacity(doc.as_string().len() + 1024);
676
677    for item_or_other in markdown_link_iterator(&doc.markdown).complete() {
678        match item_or_other {
679            ItemOrOther::Item(MarkdownLink::Inline { link: inline_link }) => {
680                let markdown_link: MarkdownLinkAction = match strip_links {
681                    false => markdown_link(
682                        &inline_link.link,
683                        symbols_type,
684                        crate_name,
685                        emit_warning,
686                        config,
687                    ),
688                    true => match inline_link.link.link_as_item_path() {
689                        None => MarkdownLinkAction::Preserve,
690                        Some(_) => MarkdownLinkAction::Strip,
691                    },
692                };
693
694                match markdown_link {
695                    MarkdownLinkAction::Link(markdown_link) => {
696                        new_doc.push_str(&inline_link.with_link(markdown_link).to_string());
697                    }
698                    MarkdownLinkAction::Preserve => {
699                        new_doc.push_str(&inline_link.to_string());
700                    }
701                    MarkdownLinkAction::Strip => {
702                        new_doc.push_str(&inline_link.text);
703                    }
704                }
705            }
706            ItemOrOther::Item(MarkdownLink::Reference { link }) => {
707                match reference_links_to_remove.contains(link.label()) {
708                    true => new_doc.push_str(link.text()),
709                    false => new_doc.push_str(&link.to_string()),
710                }
711            }
712            ItemOrOther::Other(other) => {
713                new_doc.push_str(other);
714            }
715        }
716    }
717
718    Doc::from_str(new_doc)
719}
720
721struct RewriteReferenceLinksResult {
722    doc: Doc,
723    reference_links_to_remove: HashSet<UniCase<String>>,
724}
725
726fn rewrite_reference_links_definitions(
727    doc: &Doc,
728    symbols_type: &HashMap<ItemPath, SymbolType>,
729    crate_name: &str,
730    emit_warning: &impl Fn(&str),
731    config: &IntralinksConfig,
732) -> RewriteReferenceLinksResult {
733    use crate::utils::ItemOrOther;
734    let mut reference_links_to_remove = HashSet::new();
735    let mut new_doc = String::with_capacity(doc.as_string().len() + 1024);
736    let mut skip_next_newline = false;
737    let strip_links = config.strip_links.unwrap_or(false);
738
739    let iter = markdown_reference_link_definition_iterator(&doc.markdown);
740
741    for item_or_other in iter.complete() {
742        match item_or_other {
743            ItemOrOther::Item(link_ref_def) => {
744                let markdown_link: MarkdownLinkAction = match strip_links {
745                    false => markdown_link(
746                        &link_ref_def.link,
747                        symbols_type,
748                        crate_name,
749                        emit_warning,
750                        config,
751                    ),
752                    true => match link_ref_def.link.link_as_item_path() {
753                        None => MarkdownLinkAction::Preserve,
754                        Some(_) => MarkdownLinkAction::Strip,
755                    },
756                };
757
758                match markdown_link {
759                    MarkdownLinkAction::Link(link) => {
760                        new_doc.push_str(&link_ref_def.with_link(link).to_string());
761                    }
762                    MarkdownLinkAction::Preserve => {
763                        new_doc.push_str(&link_ref_def.to_string());
764                    }
765                    MarkdownLinkAction::Strip => {
766                        // Do not emit anything to new_doc.
767                        reference_links_to_remove.insert(link_ref_def.label);
768                        skip_next_newline = true;
769                    }
770                }
771            }
772            ItemOrOther::Other(other) => {
773                let other = match skip_next_newline {
774                    true => {
775                        skip_next_newline = false;
776                        let next_index = other
777                            .chars()
778                            .enumerate()
779                            .skip_while(|(_, c)| c.is_whitespace() && *c != '\n')
780                            .skip(1)
781                            .map(|(i, _)| i)
782                            .next();
783
784                        next_index.and_then(|i| other.get(i..)).unwrap_or("")
785                    }
786                    false => other,
787                };
788                new_doc.push_str(other);
789            }
790        }
791    }
792
793    RewriteReferenceLinksResult { doc: Doc::from_str(new_doc), reference_links_to_remove }
794}
795
796fn get_rustc_sysroot_libraries_dir() -> Result<PathBuf, IntralinkError> {
797    use std::process::Command;
798
799    let output = Command::new("rustc")
800        .args(["--print=sysroot"])
801        .output()
802        .map_err(|e| IntralinkError::LoadStdLibError(format!("failed to run rustc: {e}")))?;
803
804    let s = String::from_utf8(output.stdout).expect("unexpected output from rustc");
805    let sysroot = PathBuf::from(s.trim());
806    let src_path = sysroot.join("lib").join("rustlib").join("src").join("rust").join("library");
807
808    match src_path.is_dir() {
809        false => Err(IntralinkError::LoadStdLibError(format!(
810            "Cannot find rust standard library in \"{}\"",
811            src_path.display()
812        ))),
813        true => Ok(src_path),
814    }
815}
816
817#[derive(Debug)]
818struct Crate {
819    name: String,
820    entrypoint: PathBuf,
821}
822
823fn references_standard_library(symbols: &HashSet<ItemPath>) -> bool {
824    // The only way to reference standard libraries that we support is with a intra-link of form `::⋯`.
825    symbols.iter().any(|symbol| symbol.anchor == ItemPathAnchor::Root)
826}
827
828fn get_standard_libraries() -> Result<Vec<Crate>, IntralinkError> {
829    let libraries_dir = get_rustc_sysroot_libraries_dir()?;
830    let mut std_libs = Vec::with_capacity(64);
831
832    for entry in std::fs::read_dir(libraries_dir)? {
833        let entry = entry?;
834        let project_dir_path = entry.path();
835        let cargo_manifest_path = project_dir_path.join("Cargo.toml");
836        let lib_entrypoint = project_dir_path.join("src").join("lib.rs");
837
838        if cargo_manifest_path.is_file() && lib_entrypoint.is_file() {
839            let crate_name =
840                crate::project_package_name(&cargo_manifest_path).ok_or_else(|| {
841                    IntralinkError::LoadStdLibError(format!(
842                        "failed to load manifest in \"{}\"",
843                        cargo_manifest_path.display()
844                    ))
845                })?;
846            let crate_info = Crate { name: crate_name, entrypoint: lib_entrypoint };
847
848            std_libs.push(crate_info);
849        }
850    }
851
852    Ok(std_libs)
853}
854
855#[allow(clippy::too_many_lines)]
856#[cfg(test)]
857mod tests {
858    use super::*;
859    use indoc::indoc;
860    use module_walker::walk_module_items;
861    use std::cell::RefCell;
862
863    fn item_path(id: &str) -> ItemPath {
864        ItemPath::from_string(id).unwrap()
865    }
866
867    #[test]
868    fn test_item_path_is_toplevel() {
869        assert!(!item_path("crate::baz::mumble").is_toplevel());
870        assert!(!item_path("::std::baz::mumble").is_toplevel());
871        assert!(!item_path("crate::baz").is_toplevel());
872        assert!(!item_path("::std::baz").is_toplevel());
873        assert!(item_path("crate").is_toplevel());
874        assert!(item_path("::std").is_toplevel());
875    }
876
877    #[test]
878    fn test_item_path_parent() {
879        assert_eq!(item_path("crate::baz::mumble").parent(), Some(item_path("crate::baz")));
880        assert_eq!(item_path("::std::baz::mumble").parent(), Some(item_path("::std::baz")));
881        assert_eq!(item_path("crate::baz").parent(), Some(item_path("crate")));
882        assert_eq!(item_path("::std::baz").parent(), Some(item_path("::std")));
883        assert_eq!(item_path("crate").parent(), None);
884        assert_eq!(item_path("::std").parent(), None);
885    }
886
887    #[test]
888    fn test_item_path_join() {
889        assert_eq!(item_path("crate::foo").join(&"bar"), item_path("crate::foo::bar"),);
890        assert_eq!(item_path("::std::foo").join(&"bar"), item_path("::std::foo::bar"),);
891
892        assert_eq!(
893            item_path("::std::foo::bar").parent().unwrap().join(&"baz"),
894            item_path("::std::foo::baz"),
895        );
896    }
897
898    #[test]
899    fn test_all_ancestor_paths() {
900        let symbols = [
901            item_path("crate::foo::bar::baz"),
902            item_path("crate::baz::mumble"),
903            item_path("::std::vec::Vec"),
904        ];
905        let expected: HashSet<ItemPath> = [
906            item_path("crate"),
907            item_path("crate::foo"),
908            item_path("crate::foo::bar"),
909            item_path("crate::baz"),
910            item_path("::std"),
911            item_path("::std::vec"),
912        ]
913        .into_iter()
914        .collect();
915
916        assert_eq!(all_ancestor_paths(symbols.iter()), expected);
917    }
918
919    fn explore_crate(
920        ast: &[Item],
921        dir: &Path,
922        crate_symbol: &ItemPath,
923        should_explore_module: impl Fn(&ItemPath) -> bool,
924        symbols_type: &mut HashMap<ItemPath, SymbolType>,
925        emit_warning: impl Fn(&str),
926    ) {
927        let mut modules_visited: HashSet<ItemPath> = HashSet::new();
928
929        symbols_type.insert(crate_symbol.clone(), SymbolType::Crate);
930
931        let mut visit = |module: &ItemPath, item: &Item| {
932            visit_module_item(|_| true, symbols_type, module, item);
933        };
934
935        let mut explore_module = |mod_symbol: &ItemPath, mod_item: &ItemMod| -> bool {
936            check_explore_module(&should_explore_module, &mut modules_visited, mod_symbol, mod_item)
937        };
938
939        walk_module_items(ast, dir, crate_symbol, &mut visit, &mut explore_module, &emit_warning)
940            .ok()
941            .unwrap();
942    }
943
944    #[test]
945    fn test_walk_module_and_symbols_type() {
946        let module_skip: ItemPath = item_path("crate::skip");
947
948        let source = indoc! { "
949            struct AStruct {}
950
951            mod skip {
952              struct Skip {}
953            }
954
955            mod a {
956              mod b {
957                trait ATrait {}
958              }
959
960              struct FooStruct {}
961            }
962            "
963        };
964
965        let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
966        let warnings = RefCell::new(Vec::new());
967
968        explore_crate(
969            &syn::parse_file(source).unwrap().items,
970            &PathBuf::new(),
971            &item_path("crate"),
972            |m| *m != module_skip,
973            &mut symbols_type,
974            |msg| warnings.borrow_mut().push(msg.to_owned()),
975        );
976
977        let expected: HashMap<ItemPath, SymbolType> = [
978            (item_path("crate"), SymbolType::Crate),
979            (item_path("crate::AStruct"), SymbolType::Struct),
980            (item_path("crate::skip"), SymbolType::Mod),
981            (item_path("crate::a"), SymbolType::Mod),
982            (item_path("crate::a::b"), SymbolType::Mod),
983            (item_path("crate::a::b::ATrait"), SymbolType::Trait),
984            (item_path("crate::a::FooStruct"), SymbolType::Struct),
985        ]
986        .into_iter()
987        .collect();
988
989        assert_eq!(symbols_type, expected);
990    }
991
992    #[test]
993    fn test_symbols_type_with_mod_under_cfg_test() {
994        let source = indoc! { "
995            #[cfg(not(test))]
996            mod a {
997              struct MyStruct {}
998            }
999
1000            #[cfg(test)]
1001            mod a {
1002              struct MyStructTest {}
1003            }
1004
1005            #[cfg(test)]
1006            mod b {
1007              struct MyStructTest {}
1008            }
1009
1010            #[cfg(not(test))]
1011            mod b {
1012              struct MyStruct {}
1013            }
1014            "
1015        };
1016
1017        let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1018        let warnings = RefCell::new(Vec::new());
1019
1020        explore_crate(
1021            &syn::parse_file(source).unwrap().items,
1022            &PathBuf::new(),
1023            &item_path("crate"),
1024            |_| true,
1025            &mut symbols_type,
1026            |msg| warnings.borrow_mut().push(msg.to_owned()),
1027        );
1028
1029        let expected: HashMap<ItemPath, SymbolType> = [
1030            (item_path("crate"), SymbolType::Crate),
1031            (item_path("crate::a"), SymbolType::Mod),
1032            (item_path("crate::a::MyStruct"), SymbolType::Struct),
1033            (item_path("crate::b"), SymbolType::Mod),
1034            (item_path("crate::b::MyStruct"), SymbolType::Struct),
1035        ]
1036        .into_iter()
1037        .collect();
1038
1039        assert_eq!(symbols_type, expected);
1040    }
1041
1042    #[test]
1043    fn test_symbols_type_multiple_module_first_wins() {
1044        let source = indoc! { "
1045            #[cfg(not(foo))]
1046            mod a {
1047              struct MyStruct {}
1048            }
1049
1050            #[cfg(foo)]
1051            mod a {
1052              struct Skip {}
1053            }
1054            "
1055        };
1056
1057        let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1058        let warnings = RefCell::new(Vec::new());
1059
1060        explore_crate(
1061            &syn::parse_file(source).unwrap().items,
1062            &PathBuf::new(),
1063            &item_path("crate"),
1064            |_| true,
1065            &mut symbols_type,
1066            |msg| warnings.borrow_mut().push(msg.to_owned()),
1067        );
1068
1069        let expected: HashMap<ItemPath, SymbolType> = [
1070            (item_path("crate"), SymbolType::Crate),
1071            (item_path("crate::a"), SymbolType::Mod),
1072            (item_path("crate::a::MyStruct"), SymbolType::Struct),
1073        ]
1074        .into_iter()
1075        .collect();
1076
1077        assert_eq!(symbols_type, expected);
1078    }
1079
1080    #[test]
1081    fn test_traverse_module_expore_lazily() {
1082        let symbols: HashSet<ItemPath> = [item_path("crate::module")].into_iter().collect();
1083        let modules = all_ancestor_paths(symbols.iter());
1084
1085        let source = indoc! { "
1086            mod module {
1087              struct Foo {}
1088            }
1089            "
1090        };
1091
1092        let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1093        let warnings = RefCell::new(Vec::new());
1094
1095        explore_crate(
1096            &syn::parse_file(source).unwrap().items,
1097            &PathBuf::new(),
1098            &item_path("crate"),
1099            |module| modules.contains(module),
1100            &mut symbols_type,
1101            |msg| warnings.borrow_mut().push(msg.to_owned()),
1102        );
1103
1104        let symbols_type: HashSet<ItemPath> = symbols_type.keys().cloned().collect();
1105
1106        // We should still get `crate::module`, but nothing inside it.
1107        let expected: HashSet<ItemPath> =
1108            [item_path("crate"), item_path("crate::module")].into_iter().collect();
1109
1110        assert_eq!(symbols_type, expected);
1111    }
1112
1113    #[test]
1114    fn test_documentation_url() {
1115        let config = IntralinksDocsRsConfig::default();
1116
1117        let symbols_type: HashMap<ItemPath, SymbolType> =
1118            [(item_path("crate"), SymbolType::Crate)].into_iter().collect();
1119
1120        let link = documentation_url(&item_path("crate"), &symbols_type, "foobini", None, &config);
1121        assert_eq!(link.as_deref(), Some("https://docs.rs/foobini/latest/foobini/"));
1122
1123        let symbols_type: HashMap<ItemPath, SymbolType> =
1124            [(item_path("crate::AStruct"), SymbolType::Struct)].into_iter().collect();
1125
1126        let link = documentation_url(
1127            &item_path("crate::AStruct"),
1128            &symbols_type,
1129            "foobini",
1130            None,
1131            &config,
1132        );
1133        assert_eq!(
1134            link.as_deref(),
1135            Some("https://docs.rs/foobini/latest/foobini/struct.AStruct.html")
1136        );
1137
1138        let symbols_type: HashMap<ItemPath, SymbolType> =
1139            [(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
1140
1141        let link = documentation_url(
1142            &item_path("crate::amodule"),
1143            &symbols_type,
1144            "foobini",
1145            None,
1146            &config,
1147        );
1148        assert_eq!(link.as_deref(), Some("https://docs.rs/foobini/latest/foobini/amodule/"));
1149
1150        let symbols_type: HashMap<ItemPath, SymbolType> =
1151            [(item_path("::std"), SymbolType::Crate)].into_iter().collect();
1152
1153        let link = documentation_url(&item_path("::std"), &symbols_type, "foobini", None, &config);
1154        assert_eq!(link.as_deref(), Some("https://doc.rust-lang.org/stable/std/"));
1155
1156        let symbols_type: HashMap<ItemPath, SymbolType> =
1157            [(item_path("::std::collections::HashMap"), SymbolType::Struct)].into_iter().collect();
1158
1159        let link = documentation_url(
1160            &item_path("::std::collections::HashMap"),
1161            &symbols_type,
1162            "foobini",
1163            None,
1164            &config,
1165        );
1166        assert_eq!(
1167            link.as_deref(),
1168            Some("https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html")
1169        );
1170
1171        let symbols_type: HashMap<ItemPath, SymbolType> =
1172            [(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
1173
1174        let link = documentation_url(
1175            &ItemPath::from_string("crate::amodule").unwrap(),
1176            &symbols_type,
1177            "foo-bar-mumble",
1178            None,
1179            &config,
1180        );
1181        assert_eq!(
1182            link.as_deref(),
1183            Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/amodule/")
1184        );
1185
1186        let symbols_type: HashMap<ItemPath, SymbolType> =
1187            [(item_path("crate"), SymbolType::Crate)].into_iter().collect();
1188
1189        let link = documentation_url(
1190            &ItemPath::from_string("crate").unwrap(),
1191            &symbols_type,
1192            "foo-bar-mumble",
1193            Some("#enums"),
1194            &config,
1195        );
1196        assert_eq!(
1197            link.as_deref(),
1198            Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/#enums")
1199        );
1200
1201        let symbols_type: HashMap<ItemPath, SymbolType> =
1202            [(item_path("crate::amod"), SymbolType::Mod)].into_iter().collect();
1203
1204        let link = documentation_url(
1205            &ItemPath::from_string("crate::amod").unwrap(),
1206            &symbols_type,
1207            "foo-bar-mumble",
1208            Some("#structs"),
1209            &config,
1210        );
1211        assert_eq!(
1212            link.as_deref(),
1213            Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/amod/#structs")
1214        );
1215
1216        let symbols_type: HashMap<ItemPath, SymbolType> =
1217            [(item_path("crate::MyStruct"), SymbolType::Struct)].into_iter().collect();
1218
1219        let link = documentation_url(
1220            &ItemPath::from_string("crate::MyStruct").unwrap(),
1221            &symbols_type,
1222            "foo-bar-mumble",
1223            Some("#implementations"),
1224            &config,
1225        );
1226        assert_eq!(
1227            link.as_deref(),
1228            Some(
1229                "https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/struct.MyStruct.html#implementations"
1230            )
1231        );
1232
1233        let symbols_type: HashMap<ItemPath, SymbolType> = [
1234            (item_path("crate::mymod::MyStruct"), SymbolType::Struct),
1235            (
1236                item_path("crate::mymod::MyStruct::a_method"),
1237                SymbolType::ImplItem(ImplSymbolType::Method),
1238            ),
1239        ]
1240        .into_iter()
1241        .collect();
1242
1243        let link = documentation_url(
1244            &ItemPath::from_string("crate::mymod::MyStruct::a_method").unwrap(),
1245            &symbols_type,
1246            "foo-bar-mumble",
1247            Some("#thiswillbedropped"),
1248            &config,
1249        );
1250        assert_eq!(
1251            link.as_deref(),
1252            Some(
1253                "https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/mymod/struct.MyStruct.html#method.a_method"
1254            )
1255        );
1256
1257        let config = IntralinksDocsRsConfig {
1258            docs_rs_base_url: Some("https://docs.company.rs".to_owned()),
1259            docs_rs_version: Some("1.0.0".to_owned()),
1260        };
1261
1262        let symbols_type: HashMap<ItemPath, SymbolType> =
1263            [(item_path("crate::Foo"), SymbolType::Struct)].into_iter().collect();
1264
1265        let link =
1266            documentation_url(&item_path("crate::Foo"), &symbols_type, "foobini", None, &config);
1267        assert_eq!(
1268            link.as_deref(),
1269            Some("https://docs.company.rs/foobini/1.0.0/foobini/struct.Foo.html")
1270        );
1271    }
1272
1273    #[test]
1274    fn test_extract_markdown_intralink_symbols() {
1275        let doc = indoc! { "
1276            # Foobini
1277
1278            This [beautiful crate](crate) is cool because it contains [modules](crate::amodule)
1279            and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1280
1281            Go ahead and check all the [structs in foo](crate::foo#structs).
1282            Also check [this](::std::sync::Arc) and [this](::alloc::sync::Arc).
1283
1284            We also support [reference][style] [links].
1285
1286            [style]: crate::amodule
1287            [links]: crate::foo#structs
1288            "
1289        };
1290
1291        let symbols = extract_markdown_intralink_symbols(&Doc::from_str(doc));
1292
1293        let expected: HashSet<ItemPath> = [
1294            item_path("crate"),
1295            item_path("crate::amodule"),
1296            item_path("crate::foo"),
1297            item_path("::std::sync::Arc"),
1298            item_path("::alloc::sync::Arc"),
1299        ]
1300        .into_iter()
1301        .collect();
1302
1303        assert_eq!(symbols, expected);
1304    }
1305
1306    #[test]
1307    fn test_rewrite_markdown_links() {
1308        let doc = indoc! { r"
1309            # Foobini
1310
1311            This [beautiful crate](crate) is cool because it contains [modules](crate::amodule)
1312            and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1313
1314            This link is [broken](crate::broken) and this is [not supported](::foo::bar), but this
1315            should [wor\\k \[fi\]ne](f\\i\(n\)e).
1316
1317            Go ahead and check all the [structs in foo](crate::foo#structs) specifically
1318            [this one](crate::foo::BestStruct).  Also, this is a nice function: [copy](::std::fs::copy).
1319
1320            [![BestStruct doc](https://example.com/image.png)](crate::foo::BestStruct)
1321            "
1322        };
1323
1324        let symbols_type: HashMap<ItemPath, SymbolType> = [
1325            (item_path("crate"), SymbolType::Crate),
1326            (item_path("crate::amodule"), SymbolType::Mod),
1327            (item_path("crate::foo"), SymbolType::Mod),
1328            (item_path("crate::foo::BestStruct"), SymbolType::Struct),
1329            (item_path("::std::fs::copy"), SymbolType::Fn),
1330        ]
1331        .into_iter()
1332        .collect();
1333
1334        let new_readme = rewrite_markdown_links(
1335            &Doc::from_str(doc),
1336            &symbols_type,
1337            "foobini",
1338            &|_| (),
1339            &IntralinksConfig::default(),
1340            &HashSet::new(),
1341        );
1342        let expected = indoc! { r"
1343            # Foobini
1344
1345            This [beautiful crate](https://docs.rs/foobini/latest/foobini/) is cool because it contains [modules](https://docs.rs/foobini/latest/foobini/amodule/)
1346            and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1347
1348            This link is broken and this is not supported, but this
1349            should [wor\\k \[fi\]ne](f\\i\(n\)e).
1350
1351            Go ahead and check all the [structs in foo](https://docs.rs/foobini/latest/foobini/foo/#structs) specifically
1352            [this one](https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html).  Also, this is a nice function: [copy](https://doc.rust-lang.org/stable/std/fs/fn.copy.html).
1353
1354            [![BestStruct doc](https://example.com/image.png)](https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html)
1355            "
1356        };
1357
1358        assert_eq!(new_readme.as_string(), expected);
1359    }
1360
1361    #[test]
1362    fn test_rewrite_markdown_links_strip_links() {
1363        let doc = indoc! { r"
1364            # Foobini
1365
1366            This [beautiful crate](crate) is cool because it contains [modules](crate::amodule)
1367            and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1368
1369            This link is [broken](crate::broken) and this is [not supported](::foo::bar), but this
1370            should [wor\\k \[fi\]ne](f\\i\(n\)e).
1371
1372            Go ahead and check all the [structs in foo](crate::foo#structs) specifically
1373            [this one](crate::foo::BestStruct).  Also, this is a nice function: [copy](::std::fs::copy).
1374
1375            [![BestStruct doc](https://example.com/image.png)](crate::foo::BestStruct)
1376
1377            It works with backtricks as well: [modules](`crate::amodule`).  And with
1378            [reference-style links][ref] (preserving other [references][other]).
1379
1380            [ref]: crate::foo::AnotherStruct
1381            [other]: https://en.wikipedia.org/wiki/Reference_(computer_science)
1382            "
1383        };
1384
1385        let symbols_type: HashMap<ItemPath, SymbolType> = [
1386            (item_path("crate"), SymbolType::Crate),
1387            (item_path("crate::amodule"), SymbolType::Mod),
1388            (item_path("crate::foo"), SymbolType::Mod),
1389            (item_path("crate::foo::BestStruct"), SymbolType::Struct),
1390            (item_path("crate::foo::AnotherStruct"), SymbolType::Struct),
1391        ]
1392        .into_iter()
1393        .collect();
1394
1395        let new_readme = rewrite_links(
1396            &Doc::from_str(doc),
1397            &symbols_type,
1398            "foobini",
1399            &|_| (),
1400            &IntralinksConfig { strip_links: Some(true), ..Default::default() },
1401        );
1402        let expected = indoc! { r"
1403            # Foobini
1404
1405            This beautiful crate is cool because it contains modules
1406            and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1407
1408            This link is broken and this is not supported, but this
1409            should [wor\\k \[fi\]ne](f\\i\(n\)e).
1410
1411            Go ahead and check all the structs in foo specifically
1412            this one.  Also, this is a nice function: copy.
1413
1414            ![BestStruct doc](https://example.com/image.png)
1415
1416            It works with backtricks as well: modules.  And with
1417            reference-style links (preserving other [references][other]).
1418
1419            [other]: https://en.wikipedia.org/wiki/Reference_(computer_science)
1420            "
1421        };
1422
1423        assert_eq!(new_readme.as_string(), expected);
1424    }
1425
1426    #[test]
1427    fn test_rewrite_markdown_links_backticked() {
1428        let doc = indoc! { r"
1429            # Foobini
1430
1431            This [beautiful crate](`crate`) is cool because it contains [modules](`crate::amodule`)
1432            and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1433
1434            This link is [broken](`crate::broken`) and this is [not supported](`::foo::bar`), but this
1435            should [wor\\k \[fi\]ne](f\\i\(n\)e).
1436
1437            Go ahead and check all the [structs in foo](`crate::foo#structs`) and
1438            [structs in foo](`crate::foo`#structs) specifically [this one](`crate::foo::BestStruct`).
1439            Also, this is a nice function: [copy](`::std::fs::copy`).
1440
1441            [![BestStruct doc](https://example.com/image.png)](`crate::foo::BestStruct`)
1442            "
1443        };
1444
1445        let symbols_type: HashMap<ItemPath, SymbolType> = [
1446            (item_path("crate"), SymbolType::Crate),
1447            (item_path("crate::amodule"), SymbolType::Mod),
1448            (item_path("crate::foo"), SymbolType::Mod),
1449            (item_path("crate::foo::BestStruct"), SymbolType::Struct),
1450            (item_path("::std::fs::copy"), SymbolType::Fn),
1451        ]
1452        .into_iter()
1453        .collect();
1454
1455        let new_readme = rewrite_markdown_links(
1456            &Doc::from_str(doc),
1457            &symbols_type,
1458            "foobini",
1459            &|_| (),
1460            &IntralinksConfig::default(),
1461            &HashSet::new(),
1462        );
1463        let expected = indoc! { r"
1464            # Foobini
1465
1466            This [beautiful crate](https://docs.rs/foobini/latest/foobini/) is cool because it contains [modules](https://docs.rs/foobini/latest/foobini/amodule/)
1467            and some other [stuff](https://en.wikipedia.org/wiki/Stuff) as well.
1468
1469            This link is broken and this is not supported, but this
1470            should [wor\\k \[fi\]ne](f\\i\(n\)e).
1471
1472            Go ahead and check all the [structs in foo](https://docs.rs/foobini/latest/foobini/foo/#structs) and
1473            [structs in foo](https://docs.rs/foobini/latest/foobini/foo/#structs) specifically [this one](https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html).
1474            Also, this is a nice function: [copy](https://doc.rust-lang.org/stable/std/fs/fn.copy.html).
1475
1476            [![BestStruct doc](https://example.com/image.png)](https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html)
1477            "
1478        };
1479
1480        assert_eq!(new_readme.as_string(), expected);
1481    }
1482
1483    #[test]
1484    fn test_markdown_reference_definitions() {
1485        let doc = indoc! { r#"
1486            # Foobini
1487
1488            This [beautiful crate] is cool because it contains [modules]
1489            and some other [stuff] as well.
1490
1491            This link is [broken] and this is [not supported],
1492            but this should [wor\\k \[fi\]ne].
1493
1494            Go ahead and check all the [structs in foo] specifically
1495            [this one].  Also, this is a nice function: [copy][cp].
1496
1497            [![BestStruct doc]][BestStruct]
1498
1499            [beautiful crate]: crate
1500            [modules]: crate::amodule
1501            [stuff]: https://en.wikipedia.org/wiki/Stuff
1502            [broken]: crate::broken
1503            [not supported]: ::foo::bar
1504            [wor\\k \[fi\]ne]: f\\i\(n\)e
1505            [structs in foo]: crate::foo#structs
1506            [this one]: crate::foo::BestStruct
1507            [cp]: ::std::fs::copy#examples "A title here"
1508            [BestStruct doc]: https://example.com/image.png
1509            [BestStruct]: crate::foo::BestStruct
1510            "#
1511        };
1512
1513        let symbols_type: HashMap<ItemPath, SymbolType> = [
1514            (item_path("crate"), SymbolType::Crate),
1515            (item_path("crate::amodule"), SymbolType::Mod),
1516            (item_path("crate::foo"), SymbolType::Mod),
1517            (item_path("crate::foo::BestStruct"), SymbolType::Struct),
1518            (item_path("::std::fs::copy"), SymbolType::Fn),
1519        ]
1520        .into_iter()
1521        .collect();
1522
1523        let new_readme = rewrite_links(
1524            &Doc::from_str(doc),
1525            &symbols_type,
1526            "foobini",
1527            &|_| (),
1528            &IntralinksConfig::default(),
1529        );
1530        let expected = indoc! { r#"
1531            # Foobini
1532
1533            This [beautiful crate] is cool because it contains [modules]
1534            and some other [stuff] as well.
1535
1536            This link is broken and this is not supported,
1537            but this should [wor\\k \[fi\]ne].
1538
1539            Go ahead and check all the [structs in foo] specifically
1540            [this one].  Also, this is a nice function: [copy][cp].
1541
1542            [![BestStruct doc]][BestStruct]
1543
1544            [beautiful crate]: https://docs.rs/foobini/latest/foobini/
1545            [modules]: https://docs.rs/foobini/latest/foobini/amodule/
1546            [stuff]: https://en.wikipedia.org/wiki/Stuff
1547            [wor\\k \[fi\]ne]: f\\i\(n\)e
1548            [structs in foo]: https://docs.rs/foobini/latest/foobini/foo/#structs
1549            [this one]: https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html
1550            [cp]: https://doc.rust-lang.org/stable/std/fs/fn.copy.html#examples "A title here"
1551            [BestStruct doc]: https://example.com/image.png
1552            [BestStruct]: https://docs.rs/foobini/latest/foobini/foo/struct.BestStruct.html
1553            "#
1554        };
1555
1556        assert_eq!(new_readme.as_string(), expected);
1557    }
1558
1559    #[test]
1560    fn test_rewrite_markdown_links_removes_links() {
1561        let doc = indoc! { r"
1562            # Foobini
1563
1564            This crate has multiple [modules][mod a].  This link is [broken] and [so is this][null].
1565
1566            [mod a]: crate::amodule
1567            [broken]: crate::broken
1568            [null]: crate::nothing
1569            "
1570        };
1571
1572        let symbols_type: HashMap<ItemPath, SymbolType> =
1573            [(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
1574
1575        let new_readme = rewrite_links(
1576            &Doc::from_str(doc),
1577            &symbols_type,
1578            "foobini",
1579            &|_| (),
1580            &IntralinksConfig::default(),
1581        );
1582        let expected = indoc! { r"
1583            # Foobini
1584
1585            This crate has multiple [modules][mod a].  This link is broken and so is this.
1586
1587            [mod a]: https://docs.rs/foobini/latest/foobini/amodule/
1588            "
1589        };
1590
1591        assert_eq!(new_readme.as_string(), expected);
1592    }
1593
1594    #[test]
1595    fn test_markdown_impl_items() {
1596        let doc = indoc! { r#"
1597            # Foobini
1598
1599            This crate has [`Foo::new()`](`crate::Foo::new`), [`Foo::a_method()`](`crate::Foo::a_method`),
1600            and [`Foo::another_method()`](`crate::Foo::another_method`).
1601
1602            It also has [`Foo::no_self()`](`crate::Foo::no_self`).  There's also [`Bar::beer()`](`crate::amod::Bar::beer`).
1603
1604            Struct `Foo` has a [type called `baz`](`crate::Foo::Baz`) and a
1605            [const called `number`](`crate::Foo::number`).
1606
1607            We have a function in `FooAlias` [called `hello`](`crate::FooAlias::hello`).
1608
1609            And in `MyEnum` we have [called `hey`](`crate::MyEnum::hey`).
1610
1611            And in `MyUnion` we have [called `sup`](`crate::MyUnion::sup`).
1612            "#
1613        };
1614
1615        let symbols_type: HashMap<ItemPath, SymbolType> = [
1616            (item_path("crate"), SymbolType::Crate),
1617            (item_path("crate::Foo"), SymbolType::Struct),
1618            (item_path("crate::Foo::new"), SymbolType::ImplItem(ImplSymbolType::Method)),
1619            (item_path("crate::Foo::a_method"), SymbolType::ImplItem(ImplSymbolType::Method)),
1620            (item_path("crate::Foo::another_method"), SymbolType::ImplItem(ImplSymbolType::Method)),
1621            (item_path("crate::Foo::no_self"), SymbolType::ImplItem(ImplSymbolType::Method)),
1622            (item_path("crate::amod::Bar"), SymbolType::Struct),
1623            (item_path("crate::amod::Bar::beer"), SymbolType::ImplItem(ImplSymbolType::Method)),
1624            (item_path("crate::Foo::Baz"), SymbolType::ImplItem(ImplSymbolType::Type)),
1625            (item_path("crate::Foo::number"), SymbolType::ImplItem(ImplSymbolType::Const)),
1626            (item_path("crate::FooAlias"), SymbolType::Type),
1627            (item_path("crate::FooAlias::hello"), SymbolType::ImplItem(ImplSymbolType::Method)),
1628            (item_path("crate::MyEnum"), SymbolType::Enum),
1629            (item_path("crate::MyEnum::hey"), SymbolType::ImplItem(ImplSymbolType::Method)),
1630            (item_path("crate::MyUnion"), SymbolType::Union),
1631            (item_path("crate::MyUnion::sup"), SymbolType::ImplItem(ImplSymbolType::Method)),
1632        ]
1633        .into_iter()
1634        .collect();
1635
1636        let new_readme = rewrite_links(
1637            &Doc::from_str(doc),
1638            &symbols_type,
1639            "foobini",
1640            &|_| (),
1641            &IntralinksConfig::default(),
1642        );
1643        let expected = indoc! { r#"
1644            # Foobini
1645
1646            This crate has [`Foo::new()`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#method.new), [`Foo::a_method()`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#method.a_method),
1647            and [`Foo::another_method()`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#method.another_method).
1648
1649            It also has [`Foo::no_self()`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#method.no_self).  There's also [`Bar::beer()`](https://docs.rs/foobini/latest/foobini/amod/struct.Bar.html#method.beer).
1650
1651            Struct `Foo` has a [type called `baz`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#associatedtype.Baz) and a
1652            [const called `number`](https://docs.rs/foobini/latest/foobini/struct.Foo.html#associatedconstant.number).
1653
1654            We have a function in `FooAlias` [called `hello`](https://docs.rs/foobini/latest/foobini/type.FooAlias.html#method.hello).
1655
1656            And in `MyEnum` we have [called `hey`](https://docs.rs/foobini/latest/foobini/enum.MyEnum.html#method.hey).
1657
1658            And in `MyUnion` we have [called `sup`](https://docs.rs/foobini/latest/foobini/union.MyUnion.html#method.sup).
1659            "#
1660        };
1661
1662        assert_eq!(new_readme.as_string(), expected);
1663    }
1664}