Skip to main content

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 {
167            let r = s.strip_prefix("crate::")?;
168            anchor = ItemPathAnchor::Crate;
169            rest = r;
170        }
171
172        if rest.is_empty() {
173            return None;
174        }
175
176        let path: Rc<Vec<String>> = Rc::new(rest.split("::").map(str::to_owned).collect());
177
178        Some(ItemPath { anchor, path_end: path.len(), path_shared: path })
179    }
180
181    fn path_components(&self) -> impl Iterator<Item = &str> {
182        self.path_shared[0..self.path_end].iter().map(String::as_str)
183    }
184
185    fn is_toplevel(&self) -> bool {
186        match self.anchor {
187            ItemPathAnchor::Root => self.path_end <= 1,
188            ItemPathAnchor::Crate => self.path_end == 0,
189        }
190    }
191
192    fn parent(mut self) -> Option<ItemPath> {
193        match self.is_toplevel() {
194            true => None,
195            false => {
196                self.path_end -= 1;
197                Some(self)
198            }
199        }
200    }
201
202    fn name(&self) -> Option<&str> {
203        self.path_end.checked_sub(1).and_then(|i| self.path_shared.get(i)).map(String::as_str)
204    }
205
206    fn join(mut self, s: &impl ToString) -> ItemPath {
207        let path = Rc::make_mut(&mut self.path_shared);
208        path.truncate(self.path_end);
209        path.push(s.to_string());
210        self.path_end += 1;
211        self
212    }
213
214    fn all_ancestors(&self) -> impl Iterator<Item = ItemPath> + use<> {
215        let first_ancestor = self.clone().parent();
216
217        std::iter::successors(first_ancestor, |ancestor| ancestor.clone().parent())
218    }
219}
220
221impl PartialEq for ItemPath {
222    fn eq(&self, other: &Self) -> bool {
223        self.anchor == other.anchor && self.path_components().eq(other.path_components())
224    }
225}
226
227impl Eq for ItemPath {}
228
229impl Hash for ItemPath {
230    fn hash<H: Hasher>(&self, state: &mut H) {
231        self.anchor.hash(state);
232        self.path_components().for_each(|c| c.hash(state));
233    }
234}
235
236impl fmt::Display for ItemPath {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
238        match self.anchor {
239            ItemPathAnchor::Root => (),
240            ItemPathAnchor::Crate => f.write_str("crate")?,
241        }
242
243        for s in self.path_components() {
244            f.write_str("::")?;
245            f.write_str(s)?;
246        }
247
248        Ok(())
249    }
250}
251
252impl fmt::Debug for ItemPath {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
254        fmt::Display::fmt(self, f)
255    }
256}
257
258#[derive(Copy, Clone, Debug, PartialEq, Eq)]
259pub enum ImplSymbolType {
260    Method,
261    Const,
262    Type,
263}
264
265#[derive(Copy, Clone, Debug, PartialEq, Eq)]
266pub enum SymbolType {
267    Crate,
268    Struct,
269    Trait,
270    Enum,
271    Union,
272    Type,
273    Mod,
274    Macro,
275    Const,
276    Fn,
277    Static,
278    ImplItem(ImplSymbolType),
279}
280
281impl SymbolType {
282    /// Returns the path of the module where this item is defined.
283    ///
284    /// Importantly, if inse module `crate::amod` we have a `struct Foo` with method `Foo::method()`,
285    /// this will `Foo::method()` return `crate::amod`.
286    fn get_module_path(self, path: &ItemPath) -> Option<ItemPath> {
287        match self {
288            SymbolType::Crate => {
289                assert!(path.is_toplevel(), "a crate should always be in a toplevel path");
290                None
291            }
292            SymbolType::Struct
293            | SymbolType::Trait
294            | SymbolType::Enum
295            | SymbolType::Union
296            | SymbolType::Type
297            | SymbolType::Mod
298            | SymbolType::Macro
299            | SymbolType::Const
300            | SymbolType::Fn
301            | SymbolType::Static => {
302                let p = path.clone().parent().unwrap_or_else(|| {
303                    panic!("item {path} of type {self:?} should have a parent module")
304                });
305                Some(p)
306            }
307            SymbolType::ImplItem(_) => {
308                let p = path
309                    .clone()
310                    .parent()
311                    .unwrap_or_else(|| {
312                        panic!("item {path} of type {self:?} should have a parent type")
313                    })
314                    .parent()
315                    .unwrap_or_else(|| {
316                        panic!("item {path} of type {self:?} should have a parent module")
317                    });
318                Some(p)
319            }
320        }
321    }
322}
323
324fn symbols_type_impl_block(
325    module: &ItemPath,
326    impl_block: &syn::ItemImpl,
327) -> Vec<(ItemPath, SymbolType)> {
328    use syn::{ImplItem, Type, TypePath};
329
330    if let Type::Path(TypePath { qself: None, path }) = &*impl_block.self_ty {
331        if let Some(self_ident) = path.get_ident().map(ToString::to_string) {
332            let self_path = module.clone().join(&self_ident);
333
334            return impl_block
335                .items
336                .iter()
337                .filter_map(|item| match item {
338                    ImplItem::Fn(m) => {
339                        let ident = m.sig.ident.to_string();
340
341                        Some((ident, ImplSymbolType::Method))
342                    }
343                    ImplItem::Const(c) => {
344                        let ident = c.ident.to_string();
345
346                        Some((ident, ImplSymbolType::Const))
347                    }
348                    ImplItem::Type(t) => {
349                        let ident = t.ident.to_string();
350
351                        Some((ident, ImplSymbolType::Type))
352                    }
353                    _ => None,
354                })
355                .map(|(ident, tpy)| (self_path.clone().join(&ident), SymbolType::ImplItem(tpy)))
356                .collect();
357        }
358    }
359
360    Vec::new()
361}
362
363fn item_symbols_type(module: &ItemPath, item: &Item) -> Vec<(ItemPath, SymbolType)> {
364    let item_path = |ident: &syn::Ident| module.clone().join(ident);
365
366    let (path, symbol_type) = match item {
367        Item::Enum(e) => (item_path(&e.ident), SymbolType::Enum),
368        Item::Struct(s) => (item_path(&s.ident), SymbolType::Struct),
369        Item::Trait(t) => (item_path(&t.ident), SymbolType::Trait),
370        Item::Union(u) => (item_path(&u.ident), SymbolType::Union),
371        Item::Type(t) => (item_path(&t.ident), SymbolType::Type),
372        Item::Mod(m) => (item_path(&m.ident), SymbolType::Mod),
373        Item::Macro(syn::ItemMacro { ident: Some(ident), .. }) => {
374            (item_path(ident), SymbolType::Macro)
375        }
376        Item::Const(c) => (item_path(&c.ident), SymbolType::Const),
377        Item::Fn(f) => (item_path(&f.sig.ident), SymbolType::Fn),
378        Item::Static(s) => (item_path(&s.ident), SymbolType::Static),
379        Item::Impl(impl_block) => {
380            return symbols_type_impl_block(module, impl_block);
381        }
382
383        _ => return Vec::new(),
384    };
385
386    vec![(path, symbol_type)]
387}
388
389fn is_cfg_test(attribute: &syn::Attribute) -> bool {
390    let test_attribute: syn::Attribute = syn::parse_quote!(#[cfg(test)]);
391
392    *attribute == test_attribute
393}
394
395fn visit_module_item(
396    save_symbol: impl Fn(&ItemPath) -> bool,
397    symbols_type: &mut HashMap<ItemPath, SymbolType>,
398    module: &ItemPath,
399    item: &Item,
400) {
401    for (symbol, symbol_type) in item_symbols_type(module, item) {
402        if save_symbol(&symbol) {
403            symbols_type.insert(symbol, symbol_type);
404        }
405    }
406}
407
408/// Returns whether we should explore a module.
409fn check_explore_module(
410    should_explore_module: impl Fn(&ItemPath) -> bool,
411    modules_visited: &mut HashSet<ItemPath>,
412    mod_symbol: &ItemPath,
413    mod_item: &ItemMod,
414) -> bool {
415    // Conditional compilation can create multiple module definitions, e.g.
416    //
417    // ```
418    // #[cfg(foo)]
419    // mod a {}
420    // #[cfg(not(foo))]
421    // mod a {}
422    // ```
423    //
424    // We choose to consider the first one only.
425    if modules_visited.contains(mod_symbol) {
426        return false;
427    }
428
429    // If a module is gated by `#[cfg(test)]` we skip it.  This happens sometimes in the
430    // standard library, and we want to explore the correct, non-test, module.
431    if mod_item.attrs.iter().any(is_cfg_test) {
432        return false;
433    }
434
435    let explore = should_explore_module(mod_symbol);
436
437    if explore {
438        modules_visited.insert(mod_symbol.clone());
439    }
440
441    explore
442}
443
444fn explore_crate<P: AsRef<Path>>(
445    file: P,
446    crate_symbol: &ItemPath,
447    symbols: &HashSet<ItemPath>,
448    paths_to_explore: &HashSet<ItemPath>,
449    symbols_type: &mut HashMap<ItemPath, SymbolType>,
450    emit_warning: &impl Fn(&str),
451) -> Result<(), module_walker::ModuleWalkError> {
452    let mut modules_visited: HashSet<ItemPath> = HashSet::new();
453
454    // Walking the module only visits items, which means we need to add the root `crate` explicitly.
455    symbols_type.insert(crate_symbol.clone(), SymbolType::Crate);
456
457    let mut visit = |module: &ItemPath, item: &Item| {
458        let save_symbol = |symbol: &ItemPath| {
459            // We also check if it belongs to the paths to explore because of impl items (e.g. a
460            // method `Foo::method` we need to know about `Foo` type.  For instance if `Foo` is a
461            // struct then the link will be `⋯/struct.Foo.html#method.method`.
462            symbols.contains(symbol) || paths_to_explore.contains(symbol)
463        };
464
465        visit_module_item(save_symbol, symbols_type, module, item);
466    };
467
468    let mut explore_module = |mod_symbol: &ItemPath, mod_item: &ItemMod| -> bool {
469        check_explore_module(
470            |mod_symbol| paths_to_explore.contains(mod_symbol),
471            &mut modules_visited,
472            mod_symbol,
473            mod_item,
474        )
475    };
476
477    walk_module_file(file, crate_symbol, &mut visit, &mut explore_module, emit_warning)
478}
479
480fn load_symbols_type<P: AsRef<Path>>(
481    entry_point: P,
482    symbols: &HashSet<ItemPath>,
483    emit_warning: &impl Fn(&str),
484) -> Result<HashMap<ItemPath, SymbolType>, IntralinkError> {
485    let paths_to_explore: HashSet<ItemPath> = all_ancestor_paths(symbols.iter());
486    let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
487
488    // Only load standard library information if needed.
489    let std_lib_crates = match references_standard_library(symbols) {
490        true => get_standard_libraries()?,
491        false => Vec::new(),
492    };
493
494    for Crate { name, entrypoint } in std_lib_crates {
495        explore_crate(
496            entrypoint,
497            &ItemPath::root(&name),
498            symbols,
499            &paths_to_explore,
500            &mut symbols_type,
501            emit_warning,
502        )?;
503    }
504
505    explore_crate(
506        entry_point,
507        &ItemPath::new(ItemPathAnchor::Crate),
508        symbols,
509        &paths_to_explore,
510        &mut symbols_type,
511        emit_warning,
512    )?;
513
514    Ok(symbols_type)
515}
516
517/// Create a set with all ancestor paths of `symbols`.  For instance, if `symbols` is
518/// `{crate::foo::bar::baz, crate::baz::mumble}` it will return
519/// `{crate, crate::foo, crate::foo::bar, crate::baz}`.
520fn all_ancestor_paths<'a>(symbols: impl Iterator<Item = &'a ItemPath>) -> HashSet<ItemPath> {
521    symbols.into_iter().flat_map(ItemPath::all_ancestors).collect()
522}
523
524fn extract_markdown_intralink_symbols(doc: &Doc) -> HashSet<ItemPath> {
525    let item_paths_inline_links =
526        markdown_link_iterator(&doc.markdown).items().filter_map(|l| match l {
527            MarkdownLink::Inline { link: inline_link } => inline_link.link.link_as_item_path(),
528            MarkdownLink::Reference { .. } => None,
529        });
530
531    let item_paths_reference_link_def = markdown_reference_link_definition_iterator(&doc.markdown)
532        .items()
533        .filter_map(|l| l.link.link_as_item_path());
534
535    item_paths_inline_links.chain(item_paths_reference_link_def).collect()
536}
537
538/// Returns the url for the item.
539///
540/// This returns `None` if the item type(s) was not successfully resolved.
541fn documentation_url(
542    item_path: &ItemPath,
543    symbols_type: &HashMap<ItemPath, SymbolType>,
544    crate_name: &str,
545    fragment: Option<&str>,
546    config: &IntralinksDocsRsConfig,
547) -> Option<String> {
548    let package_name = crate_name.replace('-', "_");
549    let typ = *symbols_type.get(item_path)?;
550
551    let mut link = match item_path.anchor {
552        ItemPathAnchor::Root => {
553            let std_crate_name =
554                item_path.path_components().next().expect("a root path should not be empty");
555            format!("https://doc.rust-lang.org/stable/{std_crate_name}/")
556        }
557        ItemPathAnchor::Crate => {
558            let base_url =
559                config.docs_rs_base_url.as_ref().map_or("https://docs.rs", String::as_str);
560            let version = config.docs_rs_version.as_ref().map_or("latest", String::as_str);
561
562            format!("{base_url}/{crate_name}/{version}/{package_name}/")
563        }
564    };
565
566    if typ == SymbolType::Crate {
567        return Some(format!("{}{}", link, fragment.unwrap_or("")));
568    }
569
570    let skip_components = match item_path.anchor {
571        ItemPathAnchor::Root => 1,
572        ItemPathAnchor::Crate => 0,
573    };
574
575    let module_path = typ.get_module_path(item_path).expect("item should belong to a module");
576
577    for s in module_path.path_components().skip(skip_components) {
578        link.push_str(s);
579        link.push('/');
580    }
581
582    let name =
583        item_path.name().unwrap_or_else(|| panic!("failed to get last component of {item_path}"));
584
585    match typ {
586        SymbolType::Crate => unreachable!(),
587        SymbolType::Struct => write!(&mut link, "struct.{name}.html"),
588        SymbolType::Trait => write!(&mut link, "trait.{name}.html"),
589        SymbolType::Enum => write!(&mut link, "enum.{name}.html"),
590        SymbolType::Union => write!(&mut link, "union.{name}.html"),
591        SymbolType::Type => write!(&mut link, "type.{name}.html"),
592        SymbolType::Mod => write!(&mut link, "{name}/"),
593        SymbolType::Macro => write!(&mut link, "macro.{name}.html"),
594        SymbolType::Const => write!(&mut link, "const.{name}.html"),
595        SymbolType::Fn => write!(&mut link, "fn.{name}.html"),
596        SymbolType::Static => write!(&mut link, "static.{name}.html"),
597        SymbolType::ImplItem(typ) => {
598            let parent_path = item_path
599                .clone()
600                .parent()
601                .unwrap_or_else(|| panic!("item {item_path} should always have a parent"));
602
603            let link = documentation_url(
604                &parent_path,
605                symbols_type,
606                crate_name,
607                // We discard the fragment.
608                None,
609                config,
610            )?;
611
612            let impl_item_fragment_str = match typ {
613                ImplSymbolType::Method => "method",
614                ImplSymbolType::Const => "associatedconstant",
615                ImplSymbolType::Type => "associatedtype",
616            };
617
618            return Some(format!("{link}#{impl_item_fragment_str}.{name}"));
619        }
620    }
621    .expect("this should never fail");
622
623    Some(format!("{}{}", link, fragment.unwrap_or("")))
624}
625
626enum MarkdownLinkAction {
627    Link(Link),
628    Preserve,
629    Strip,
630}
631
632fn markdown_link(
633    link: &Link,
634    symbols_type: &HashMap<ItemPath, SymbolType>,
635    crate_name: &str,
636    emit_warning: &impl Fn(&str),
637    config: &IntralinksConfig,
638) -> MarkdownLinkAction {
639    match link.link_as_item_path() {
640        Some(symbol) => {
641            let link = documentation_url(
642                &symbol,
643                symbols_type,
644                crate_name,
645                link.link_fragment(),
646                &config.docs_rs,
647            );
648
649            match link {
650                Some(l) => MarkdownLinkAction::Link(l.into()),
651                None => {
652                    emit_warning(&format!("Could not resolve definition of `{symbol}`."));
653
654                    // This was an intralink, but we were not able to generate a link.
655                    MarkdownLinkAction::Strip
656                }
657            }
658        }
659        None => MarkdownLinkAction::Preserve,
660    }
661}
662
663fn rewrite_markdown_links(
664    doc: &Doc,
665    symbols_type: &HashMap<ItemPath, SymbolType>,
666    crate_name: &str,
667    emit_warning: &impl Fn(&str),
668    config: &IntralinksConfig,
669    reference_links_to_remove: &HashSet<UniCase<String>>,
670) -> Doc {
671    use crate::utils::ItemOrOther;
672
673    let strip_links = config.strip_links.unwrap_or(false);
674    let mut new_doc = String::with_capacity(doc.as_string().len() + 1024);
675
676    for item_or_other in markdown_link_iterator(&doc.markdown).complete() {
677        match item_or_other {
678            ItemOrOther::Item(MarkdownLink::Inline { link: inline_link }) => {
679                let markdown_link: MarkdownLinkAction = match strip_links {
680                    false => markdown_link(
681                        &inline_link.link,
682                        symbols_type,
683                        crate_name,
684                        emit_warning,
685                        config,
686                    ),
687                    true => match inline_link.link.link_as_item_path() {
688                        None => MarkdownLinkAction::Preserve,
689                        Some(_) => MarkdownLinkAction::Strip,
690                    },
691                };
692
693                match markdown_link {
694                    MarkdownLinkAction::Link(markdown_link) => {
695                        new_doc.push_str(&inline_link.with_link(markdown_link).to_string());
696                    }
697                    MarkdownLinkAction::Preserve => {
698                        new_doc.push_str(&inline_link.to_string());
699                    }
700                    MarkdownLinkAction::Strip => {
701                        new_doc.push_str(&inline_link.text);
702                    }
703                }
704            }
705            ItemOrOther::Item(MarkdownLink::Reference { link }) => {
706                match reference_links_to_remove.contains(link.label()) {
707                    true => new_doc.push_str(link.text()),
708                    false => new_doc.push_str(&link.to_string()),
709                }
710            }
711            ItemOrOther::Other(other) => {
712                new_doc.push_str(other);
713            }
714        }
715    }
716
717    Doc::from_str(new_doc)
718}
719
720struct RewriteReferenceLinksResult {
721    doc: Doc,
722    reference_links_to_remove: HashSet<UniCase<String>>,
723}
724
725fn rewrite_reference_links_definitions(
726    doc: &Doc,
727    symbols_type: &HashMap<ItemPath, SymbolType>,
728    crate_name: &str,
729    emit_warning: &impl Fn(&str),
730    config: &IntralinksConfig,
731) -> RewriteReferenceLinksResult {
732    use crate::utils::ItemOrOther;
733    let mut reference_links_to_remove = HashSet::new();
734    let mut new_doc = String::with_capacity(doc.as_string().len() + 1024);
735    let mut skip_next_newline = false;
736    let strip_links = config.strip_links.unwrap_or(false);
737
738    let iter = markdown_reference_link_definition_iterator(&doc.markdown);
739
740    for item_or_other in iter.complete() {
741        match item_or_other {
742            ItemOrOther::Item(link_ref_def) => {
743                let markdown_link: MarkdownLinkAction = match strip_links {
744                    false => markdown_link(
745                        &link_ref_def.link,
746                        symbols_type,
747                        crate_name,
748                        emit_warning,
749                        config,
750                    ),
751                    true => match link_ref_def.link.link_as_item_path() {
752                        None => MarkdownLinkAction::Preserve,
753                        Some(_) => MarkdownLinkAction::Strip,
754                    },
755                };
756
757                match markdown_link {
758                    MarkdownLinkAction::Link(link) => {
759                        new_doc.push_str(&link_ref_def.with_link(link).to_string());
760                    }
761                    MarkdownLinkAction::Preserve => {
762                        new_doc.push_str(&link_ref_def.to_string());
763                    }
764                    MarkdownLinkAction::Strip => {
765                        // Do not emit anything to new_doc.
766                        reference_links_to_remove.insert(link_ref_def.label);
767                        skip_next_newline = true;
768                    }
769                }
770            }
771            ItemOrOther::Other(other) => {
772                let other = match skip_next_newline {
773                    true => {
774                        skip_next_newline = false;
775                        let next_index = other
776                            .chars()
777                            .enumerate()
778                            .skip_while(|(_, c)| c.is_whitespace() && *c != '\n')
779                            .skip(1)
780                            .map(|(i, _)| i)
781                            .next();
782
783                        next_index.and_then(|i| other.get(i..)).unwrap_or("")
784                    }
785                    false => other,
786                };
787                new_doc.push_str(other);
788            }
789        }
790    }
791
792    RewriteReferenceLinksResult { doc: Doc::from_str(new_doc), reference_links_to_remove }
793}
794
795fn get_rustc_sysroot_libraries_dir() -> Result<PathBuf, IntralinkError> {
796    use std::process::Command;
797
798    let output = Command::new("rustc")
799        .args(["--print=sysroot"])
800        .output()
801        .map_err(|e| IntralinkError::LoadStdLibError(format!("failed to run rustc: {e}")))?;
802
803    let s = String::from_utf8(output.stdout).expect("unexpected output from rustc");
804    let sysroot = PathBuf::from(s.trim());
805    let src_path = sysroot.join("lib").join("rustlib").join("src").join("rust").join("library");
806
807    match src_path.is_dir() {
808        false => Err(IntralinkError::LoadStdLibError(format!(
809            "Cannot find rust standard library in \"{}\"",
810            src_path.display()
811        ))),
812        true => Ok(src_path),
813    }
814}
815
816#[derive(Debug)]
817struct Crate {
818    name: String,
819    entrypoint: PathBuf,
820}
821
822fn references_standard_library(symbols: &HashSet<ItemPath>) -> bool {
823    // The only way to reference standard libraries that we support is with a intra-link of form `::⋯`.
824    symbols.iter().any(|symbol| symbol.anchor == ItemPathAnchor::Root)
825}
826
827fn get_standard_libraries() -> Result<Vec<Crate>, IntralinkError> {
828    let libraries_dir = get_rustc_sysroot_libraries_dir()?;
829    let mut std_libs = Vec::with_capacity(64);
830
831    for entry in std::fs::read_dir(libraries_dir)? {
832        let entry = entry?;
833        let project_dir_path = entry.path();
834        let cargo_manifest_path = project_dir_path.join("Cargo.toml");
835        let lib_entrypoint = project_dir_path.join("src").join("lib.rs");
836
837        if cargo_manifest_path.is_file() && lib_entrypoint.is_file() {
838            let crate_name =
839                crate::project_package_name(&cargo_manifest_path).ok_or_else(|| {
840                    IntralinkError::LoadStdLibError(format!(
841                        "failed to load manifest in \"{}\"",
842                        cargo_manifest_path.display()
843                    ))
844                })?;
845            let crate_info = Crate { name: crate_name, entrypoint: lib_entrypoint };
846
847            std_libs.push(crate_info);
848        }
849    }
850
851    Ok(std_libs)
852}
853
854#[allow(clippy::too_many_lines)]
855#[cfg(test)]
856mod tests {
857    use super::*;
858    use indoc::indoc;
859    use module_walker::walk_module_items;
860    use std::cell::RefCell;
861
862    fn item_path(id: &str) -> ItemPath {
863        ItemPath::from_string(id).unwrap()
864    }
865
866    #[test]
867    fn test_item_path_is_toplevel() {
868        assert!(!item_path("crate::baz::mumble").is_toplevel());
869        assert!(!item_path("::std::baz::mumble").is_toplevel());
870        assert!(!item_path("crate::baz").is_toplevel());
871        assert!(!item_path("::std::baz").is_toplevel());
872        assert!(item_path("crate").is_toplevel());
873        assert!(item_path("::std").is_toplevel());
874    }
875
876    #[test]
877    fn test_item_path_parent() {
878        assert_eq!(item_path("crate::baz::mumble").parent(), Some(item_path("crate::baz")));
879        assert_eq!(item_path("::std::baz::mumble").parent(), Some(item_path("::std::baz")));
880        assert_eq!(item_path("crate::baz").parent(), Some(item_path("crate")));
881        assert_eq!(item_path("::std::baz").parent(), Some(item_path("::std")));
882        assert_eq!(item_path("crate").parent(), None);
883        assert_eq!(item_path("::std").parent(), None);
884    }
885
886    #[test]
887    fn test_item_path_join() {
888        assert_eq!(item_path("crate::foo").join(&"bar"), item_path("crate::foo::bar"),);
889        assert_eq!(item_path("::std::foo").join(&"bar"), item_path("::std::foo::bar"),);
890
891        assert_eq!(
892            item_path("::std::foo::bar").parent().unwrap().join(&"baz"),
893            item_path("::std::foo::baz"),
894        );
895    }
896
897    #[test]
898    fn test_all_ancestor_paths() {
899        let symbols = [
900            item_path("crate::foo::bar::baz"),
901            item_path("crate::baz::mumble"),
902            item_path("::std::vec::Vec"),
903        ];
904        let expected: HashSet<ItemPath> = [
905            item_path("crate"),
906            item_path("crate::foo"),
907            item_path("crate::foo::bar"),
908            item_path("crate::baz"),
909            item_path("::std"),
910            item_path("::std::vec"),
911        ]
912        .into_iter()
913        .collect();
914
915        assert_eq!(all_ancestor_paths(symbols.iter()), expected);
916    }
917
918    fn explore_crate(
919        ast: &[Item],
920        dir: &Path,
921        crate_symbol: &ItemPath,
922        should_explore_module: impl Fn(&ItemPath) -> bool,
923        symbols_type: &mut HashMap<ItemPath, SymbolType>,
924        emit_warning: impl Fn(&str),
925    ) {
926        let mut modules_visited: HashSet<ItemPath> = HashSet::new();
927
928        symbols_type.insert(crate_symbol.clone(), SymbolType::Crate);
929
930        let mut visit = |module: &ItemPath, item: &Item| {
931            visit_module_item(|_| true, symbols_type, module, item);
932        };
933
934        let mut explore_module = |mod_symbol: &ItemPath, mod_item: &ItemMod| -> bool {
935            check_explore_module(&should_explore_module, &mut modules_visited, mod_symbol, mod_item)
936        };
937
938        walk_module_items(ast, dir, crate_symbol, &mut visit, &mut explore_module, &emit_warning)
939            .ok()
940            .unwrap();
941    }
942
943    #[test]
944    fn test_walk_module_and_symbols_type() {
945        let module_skip: ItemPath = item_path("crate::skip");
946
947        let source = indoc! { "
948            struct AStruct {}
949
950            mod skip {
951              struct Skip {}
952            }
953
954            mod a {
955              mod b {
956                trait ATrait {}
957              }
958
959              struct FooStruct {}
960            }
961            "
962        };
963
964        let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
965        let warnings = RefCell::new(Vec::new());
966
967        explore_crate(
968            &syn::parse_file(source).unwrap().items,
969            &PathBuf::new(),
970            &item_path("crate"),
971            |m| *m != module_skip,
972            &mut symbols_type,
973            |msg| warnings.borrow_mut().push(msg.to_owned()),
974        );
975
976        let expected: HashMap<ItemPath, SymbolType> = [
977            (item_path("crate"), SymbolType::Crate),
978            (item_path("crate::AStruct"), SymbolType::Struct),
979            (item_path("crate::skip"), SymbolType::Mod),
980            (item_path("crate::a"), SymbolType::Mod),
981            (item_path("crate::a::b"), SymbolType::Mod),
982            (item_path("crate::a::b::ATrait"), SymbolType::Trait),
983            (item_path("crate::a::FooStruct"), SymbolType::Struct),
984        ]
985        .into_iter()
986        .collect();
987
988        assert_eq!(symbols_type, expected);
989    }
990
991    #[test]
992    fn test_symbols_type_with_mod_under_cfg_test() {
993        let source = indoc! { "
994            #[cfg(not(test))]
995            mod a {
996              struct MyStruct {}
997            }
998
999            #[cfg(test)]
1000            mod a {
1001              struct MyStructTest {}
1002            }
1003
1004            #[cfg(test)]
1005            mod b {
1006              struct MyStructTest {}
1007            }
1008
1009            #[cfg(not(test))]
1010            mod b {
1011              struct MyStruct {}
1012            }
1013            "
1014        };
1015
1016        let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1017        let warnings = RefCell::new(Vec::new());
1018
1019        explore_crate(
1020            &syn::parse_file(source).unwrap().items,
1021            &PathBuf::new(),
1022            &item_path("crate"),
1023            |_| true,
1024            &mut symbols_type,
1025            |msg| warnings.borrow_mut().push(msg.to_owned()),
1026        );
1027
1028        let expected: HashMap<ItemPath, SymbolType> = [
1029            (item_path("crate"), SymbolType::Crate),
1030            (item_path("crate::a"), SymbolType::Mod),
1031            (item_path("crate::a::MyStruct"), SymbolType::Struct),
1032            (item_path("crate::b"), SymbolType::Mod),
1033            (item_path("crate::b::MyStruct"), SymbolType::Struct),
1034        ]
1035        .into_iter()
1036        .collect();
1037
1038        assert_eq!(symbols_type, expected);
1039    }
1040
1041    #[test]
1042    fn test_symbols_type_multiple_module_first_wins() {
1043        let source = indoc! { "
1044            #[cfg(not(foo))]
1045            mod a {
1046              struct MyStruct {}
1047            }
1048
1049            #[cfg(foo)]
1050            mod a {
1051              struct Skip {}
1052            }
1053            "
1054        };
1055
1056        let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1057        let warnings = RefCell::new(Vec::new());
1058
1059        explore_crate(
1060            &syn::parse_file(source).unwrap().items,
1061            &PathBuf::new(),
1062            &item_path("crate"),
1063            |_| true,
1064            &mut symbols_type,
1065            |msg| warnings.borrow_mut().push(msg.to_owned()),
1066        );
1067
1068        let expected: HashMap<ItemPath, SymbolType> = [
1069            (item_path("crate"), SymbolType::Crate),
1070            (item_path("crate::a"), SymbolType::Mod),
1071            (item_path("crate::a::MyStruct"), SymbolType::Struct),
1072        ]
1073        .into_iter()
1074        .collect();
1075
1076        assert_eq!(symbols_type, expected);
1077    }
1078
1079    #[test]
1080    fn test_traverse_module_expore_lazily() {
1081        let symbols: HashSet<ItemPath> = [item_path("crate::module")].into_iter().collect();
1082        let modules = all_ancestor_paths(symbols.iter());
1083
1084        let source = indoc! { "
1085            mod module {
1086              struct Foo {}
1087            }
1088            "
1089        };
1090
1091        let mut symbols_type: HashMap<ItemPath, SymbolType> = HashMap::new();
1092        let warnings = RefCell::new(Vec::new());
1093
1094        explore_crate(
1095            &syn::parse_file(source).unwrap().items,
1096            &PathBuf::new(),
1097            &item_path("crate"),
1098            |module| modules.contains(module),
1099            &mut symbols_type,
1100            |msg| warnings.borrow_mut().push(msg.to_owned()),
1101        );
1102
1103        let symbols_type: HashSet<ItemPath> = symbols_type.keys().cloned().collect();
1104
1105        // We should still get `crate::module`, but nothing inside it.
1106        let expected: HashSet<ItemPath> =
1107            [item_path("crate"), item_path("crate::module")].into_iter().collect();
1108
1109        assert_eq!(symbols_type, expected);
1110    }
1111
1112    #[test]
1113    fn test_documentation_url() {
1114        let config = IntralinksDocsRsConfig::default();
1115
1116        let symbols_type: HashMap<ItemPath, SymbolType> =
1117            [(item_path("crate"), SymbolType::Crate)].into_iter().collect();
1118
1119        let link = documentation_url(&item_path("crate"), &symbols_type, "foobini", None, &config);
1120        assert_eq!(link.as_deref(), Some("https://docs.rs/foobini/latest/foobini/"));
1121
1122        let symbols_type: HashMap<ItemPath, SymbolType> =
1123            [(item_path("crate::AStruct"), SymbolType::Struct)].into_iter().collect();
1124
1125        let link = documentation_url(
1126            &item_path("crate::AStruct"),
1127            &symbols_type,
1128            "foobini",
1129            None,
1130            &config,
1131        );
1132        assert_eq!(
1133            link.as_deref(),
1134            Some("https://docs.rs/foobini/latest/foobini/struct.AStruct.html")
1135        );
1136
1137        let symbols_type: HashMap<ItemPath, SymbolType> =
1138            [(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
1139
1140        let link = documentation_url(
1141            &item_path("crate::amodule"),
1142            &symbols_type,
1143            "foobini",
1144            None,
1145            &config,
1146        );
1147        assert_eq!(link.as_deref(), Some("https://docs.rs/foobini/latest/foobini/amodule/"));
1148
1149        let symbols_type: HashMap<ItemPath, SymbolType> =
1150            [(item_path("::std"), SymbolType::Crate)].into_iter().collect();
1151
1152        let link = documentation_url(&item_path("::std"), &symbols_type, "foobini", None, &config);
1153        assert_eq!(link.as_deref(), Some("https://doc.rust-lang.org/stable/std/"));
1154
1155        let symbols_type: HashMap<ItemPath, SymbolType> =
1156            [(item_path("::std::collections::HashMap"), SymbolType::Struct)].into_iter().collect();
1157
1158        let link = documentation_url(
1159            &item_path("::std::collections::HashMap"),
1160            &symbols_type,
1161            "foobini",
1162            None,
1163            &config,
1164        );
1165        assert_eq!(
1166            link.as_deref(),
1167            Some("https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html")
1168        );
1169
1170        let symbols_type: HashMap<ItemPath, SymbolType> =
1171            [(item_path("crate::amodule"), SymbolType::Mod)].into_iter().collect();
1172
1173        let link = documentation_url(
1174            &ItemPath::from_string("crate::amodule").unwrap(),
1175            &symbols_type,
1176            "foo-bar-mumble",
1177            None,
1178            &config,
1179        );
1180        assert_eq!(
1181            link.as_deref(),
1182            Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/amodule/")
1183        );
1184
1185        let symbols_type: HashMap<ItemPath, SymbolType> =
1186            [(item_path("crate"), SymbolType::Crate)].into_iter().collect();
1187
1188        let link = documentation_url(
1189            &ItemPath::from_string("crate").unwrap(),
1190            &symbols_type,
1191            "foo-bar-mumble",
1192            Some("#enums"),
1193            &config,
1194        );
1195        assert_eq!(
1196            link.as_deref(),
1197            Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/#enums")
1198        );
1199
1200        let symbols_type: HashMap<ItemPath, SymbolType> =
1201            [(item_path("crate::amod"), SymbolType::Mod)].into_iter().collect();
1202
1203        let link = documentation_url(
1204            &ItemPath::from_string("crate::amod").unwrap(),
1205            &symbols_type,
1206            "foo-bar-mumble",
1207            Some("#structs"),
1208            &config,
1209        );
1210        assert_eq!(
1211            link.as_deref(),
1212            Some("https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/amod/#structs")
1213        );
1214
1215        let symbols_type: HashMap<ItemPath, SymbolType> =
1216            [(item_path("crate::MyStruct"), SymbolType::Struct)].into_iter().collect();
1217
1218        let link = documentation_url(
1219            &ItemPath::from_string("crate::MyStruct").unwrap(),
1220            &symbols_type,
1221            "foo-bar-mumble",
1222            Some("#implementations"),
1223            &config,
1224        );
1225        assert_eq!(
1226            link.as_deref(),
1227            Some(
1228                "https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/struct.MyStruct.html#implementations"
1229            )
1230        );
1231
1232        let symbols_type: HashMap<ItemPath, SymbolType> = [
1233            (item_path("crate::mymod::MyStruct"), SymbolType::Struct),
1234            (
1235                item_path("crate::mymod::MyStruct::a_method"),
1236                SymbolType::ImplItem(ImplSymbolType::Method),
1237            ),
1238        ]
1239        .into_iter()
1240        .collect();
1241
1242        let link = documentation_url(
1243            &ItemPath::from_string("crate::mymod::MyStruct::a_method").unwrap(),
1244            &symbols_type,
1245            "foo-bar-mumble",
1246            Some("#thiswillbedropped"),
1247            &config,
1248        );
1249        assert_eq!(
1250            link.as_deref(),
1251            Some(
1252                "https://docs.rs/foo-bar-mumble/latest/foo_bar_mumble/mymod/struct.MyStruct.html#method.a_method"
1253            )
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}