cargo_rdme/transform/intralinks/
mod.rs

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