readme_rustdocifier/
inner.rs

1use std::fmt;
2
3// ======================================================================
4// ERROR - PUBLIC
5
6/// Error returned by [`rustdocify`].
7#[derive(Clone, Debug, PartialEq)]
8pub enum Error {
9    /// Version was given to [`rustdocify`] but URL is missing a version.
10    ///
11    /// # Example
12    ///
13    /// ```markdown
14    /// [foo]: https://docs.rs/foo
15    /// ```
16    MissingVersionInUrl(String),
17
18    /// Readme has top-level header that is not first header.
19    ///
20    /// # Example
21    ///
22    /// <!-- Note: Using extra `#`:s here because rustdoc removes one. -->
23    /// ```markdown
24    /// ### First
25    /// ## Second
26    /// ```
27    NonFirstTopLevelHeader(String),
28
29    /// URL is not recognized as valid.
30    ///
31    /// This means that either URL is invalid or this crate has a bug.
32    ///
33    /// # Example
34    ///
35    /// ```markdown
36    /// [foo]: https://docs.rs/foo/*/foo/hello_world.html
37    /// ```
38    UnrecognizedUrl(String),
39
40    /// Crate name was given to [`rustdocify`] but URL has different crate name.
41    ///
42    /// # Example
43    ///
44    /// ```markdown
45    /// [foo]: https://docs.rs/foo/*/DIFFERENT_CRATE
46    /// ```
47    WrongCrateNameInUrl(String),
48
49    /// Version was given to [`rustdocify`] but URL has different version.
50    ///
51    /// # Example
52    ///
53    /// ```markdown
54    /// [foo]: https://docs.rs/foo/DIFFERENT_VERSION
55    /// ```
56    WrongVersionInUrl(String),
57}
58
59impl fmt::Display for Error {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Error::MissingVersionInUrl(url) => {
63                write!(f, "missing version in url: {}", url)
64            }
65
66            Error::NonFirstTopLevelHeader(header) => {
67                write!(f, "non-first top level header: {}", header)
68            }
69
70            Error::UnrecognizedUrl(url) => {
71                write!(f, "unrecognized url: {}", url)
72            }
73
74            Error::WrongCrateNameInUrl(url) => {
75                write!(f, "wrong crate name in url: {}", url)
76            }
77
78            Error::WrongVersionInUrl(url) => {
79                write!(f, "wrong version in url: {}", url)
80            }
81        }
82    }
83}
84
85impl std::error::Error for Error {}
86
87// ======================================================================
88// FUNCTIONS - PUBLIC
89
90/// Rustdocifies the given readme.
91///
92/// - Removes top-level header.
93/// - Changes other headers to be one level higher.
94/// - Converts `docs.rs` links of given `package_name` to rustdoc format.
95/// - Doesn't change anything within code blocks.
96/// - If `version` is given, checks that links have this exact version.
97/// - If `crate_name` is given, checks that links have this exact crate name, if any.
98///
99/// See [crate index] for an example and more details.
100///
101/// [crate index]: crate
102pub fn rustdocify(
103    readme: &str,
104    package_name: &str,
105    version: Option<&str>,
106    crate_name: Option<&str>,
107) -> Result<String, Error> {
108    let mut is_first_header = true;
109    let mut code_block_level = None;
110
111    let mut result = String::with_capacity(readme.len());
112
113    for line in readme.split_inclusive('\n') {
114        if let Some(level) = code_block_level {
115            // IN CODE BLOCK
116
117            if is_code_block_end(line, level) {
118                code_block_level = None;
119            }
120
121            result.push_str(line);
122        } else {
123            // NOT IN CODE BLOCK
124
125            code_block_level = is_code_block_start(line);
126
127            if code_block_level.is_some() {
128                result.push_str(line);
129            } else if let Some(line) = convert_header_line(line, &mut is_first_header)? {
130                result.push_str(line);
131            } else if let Some(line) = convert_link_line(line, package_name, version, crate_name)? {
132                result.push_str(&line);
133            } else {
134                result.push_str(line);
135            }
136        }
137    }
138
139    Ok(result)
140}
141
142// ======================================================================
143// FUNCTIONS - PRIVATE
144
145// Returns
146// - `Ok(Some(..))` on successful conversion
147// - `Ok(None)` if this is not a header line
148// - `Err(..)` on error
149fn convert_header_line<'a>(
150    line: &'a str,
151    is_first_header: &mut bool,
152) -> Result<Option<&'a str>, Error> {
153    let bytes = line.as_bytes();
154
155    let mut level = 0;
156    while level < line.len() && bytes[level] == b'#' {
157        level += 1;
158    }
159
160    if level > 0 && bytes[level] == b' ' {
161        if level == 1 {
162            if *is_first_header {
163                *is_first_header = false;
164                Ok(Some(""))
165            } else {
166                Err(Error::NonFirstTopLevelHeader(line.to_owned()))
167            }
168        } else {
169            *is_first_header = false;
170            Ok(Some(&line[1..]))
171        }
172    } else {
173        Ok(None)
174    }
175}
176
177// [...]: https://docs.rs/PACKAGE...
178//
179// Returns
180// - `Ok(Some(..))` on successful conversion
181// - `Ok(None)` if this is not a link line
182// - `Err(..)` on error
183fn convert_link_line(
184    line: &str,
185    package_name: &str,
186    version: Option<&str>,
187    crate_name: Option<&str>,
188) -> Result<Option<String>, Error> {
189    let bytes = line.as_bytes();
190
191    if bytes[0] != b'[' {
192        return Ok(None);
193    }
194
195    let close_bracket_pos = if let Some(pos) = line.find(']') {
196        pos
197    } else {
198        return Ok(None);
199    };
200
201    if bytes[close_bracket_pos + 1] != b':' {
202        return Ok(None);
203    }
204
205    let url_start_pos =
206        if let Some(pos) = line[close_bracket_pos + 2..].find(|c: char| !c.is_whitespace()) {
207            close_bracket_pos + 2 + pos
208        } else {
209            return Ok(None);
210        };
211
212    if url_start_pos == close_bracket_pos + 2 {
213        return Ok(None);
214    }
215
216    let url_end_pos = if let Some(pos) = line[url_start_pos..].find(|c: char| c.is_whitespace()) {
217        url_start_pos + pos
218    } else {
219        line.len()
220    };
221
222    let url = &line[url_start_pos..url_end_pos];
223
224    match convert_url(url, package_name, version, crate_name) {
225        Ok(link) => Ok(Some(format!(
226            "{}{}{}",
227            &line[..url_start_pos],
228            link,
229            &line[url_end_pos..]
230        ))),
231        Err(error) => Err(error),
232    }
233}
234
235fn convert_url(
236    url: &str,
237    package_name: &str,
238    version: Option<&str>,
239    crate_name: Option<&str>,
240) -> Result<String, Error> {
241    let url_prefix = format!("https://docs.rs/{}", package_name);
242    if !url.starts_with(&url_prefix) {
243        return Ok(url.to_owned());
244    }
245
246    // url_prefix + optional '/'
247    let url_prefix_len = if url.len() == url_prefix.len() {
248        url_prefix.len()
249    } else {
250        let byte_after_prefix = url.as_bytes()[url_prefix.len()];
251        if byte_after_prefix == b'/' {
252            url_prefix.len() + 1
253        } else if byte_after_prefix == b'#' {
254            url_prefix.len()
255        } else {
256            return Ok(url.to_owned());
257        }
258    };
259
260    let mut path: Vec<&str>;
261    let fragment;
262
263    if let Some(fragment_start_pos) = url.find('#') {
264        path = url[url_prefix_len..fragment_start_pos].split('/').collect();
265        fragment = Some(&url[fragment_start_pos + 1..]);
266    } else {
267        path = url[url_prefix_len..].split('/').collect();
268        fragment = None;
269    }
270
271    if path.last() == Some(&"") {
272        path.pop();
273    }
274
275    // VERSION
276
277    if path.is_empty() {
278        // NO VERSION IN URL
279
280        if version.is_some() {
281            return Err(Error::MissingVersionInUrl(url.to_owned()));
282        } else {
283            return Ok(root_link(fragment));
284        }
285    }
286
287    let url_version = path[0];
288
289    if let Some(version) = version {
290        if url_version != version {
291            return Err(Error::WrongVersionInUrl(url.to_owned()));
292        }
293    }
294
295    if path.len() == 2 && path[1] == "index.html" {
296        return Ok(root_link(fragment));
297    }
298
299    // CRATE NAME
300
301    if path.len() == 1 {
302        // NO CRATE NAME IN URL
303
304        return Ok(root_link(fragment));
305    }
306
307    let url_crate = path[1];
308
309    if let Some(crate_name) = crate_name {
310        if url_crate != crate_name {
311            return Err(Error::WrongCrateNameInUrl(url.to_owned()));
312        }
313    }
314
315    if path.len() == 3 && path[2] == "index.html" {
316        return Ok(root_link(fragment));
317    }
318
319    // FILENAME
320
321    if path.len() == 2 {
322        // NO FILENAME IN URL
323
324        return Ok(root_link(fragment));
325    }
326
327    let last = *path.last().unwrap();
328
329    let (modules, filename) = if last.contains('.') {
330        (&path[2..path.len() - 1], last)
331    } else {
332        (&path[2..], "index.html")
333    };
334
335    let modules = if modules.is_empty() {
336        "".to_owned()
337    } else {
338        format!("::{}", modules.join("::"))
339    };
340
341    if filename == "index.html" {
342        if let Some(fragment) = fragment {
343            return Ok(format!("crate{}#{}", modules, fragment));
344        } else {
345            return Ok(format!("crate{}", modules));
346        }
347    }
348
349    // enum.ENUM.html
350    // enum.ENUM.html#method.METHOD
351    // enum.ENUM.html#variant.VARIANT
352    // enum.ENUM.html#fragment
353    // fn.FUNCTION.html
354    // fn.FUNCTION.html#fragment
355    // macro.FUNCTION.html
356    // macro.FUNCTION.html#fragment
357    // struct.STRUCT.html
358    // struct.STRUCT.html#method.METHOD
359    // struct.STRUCT.html#fragment
360    // trait.TRAIT.html
361    // trait.TRAIT.html#tymethod.METHOD
362    // trait.TRAIT.html#fragment
363
364    const DATA: &[(&str, &[&str])] = &[
365        ("enum.", &["method.", "variant."]),
366        ("fn.", &[]),
367        ("macro.", &[]),
368        ("struct.", &["method."]),
369        ("trait.", &["tymethod."]),
370    ];
371
372    if !filename.ends_with(".html") {
373        Err(Error::UnrecognizedUrl(url.to_owned()))
374    } else {
375        for (prefix, special_fragment_starts) in DATA {
376            if filename.starts_with(prefix) {
377                let name = &filename[prefix.len()..&filename.len() - 5];
378                if let Some(fragment) = fragment {
379                    for special_fragment_start in special_fragment_starts.iter() {
380                        if let Some(fragment_name) = fragment.strip_prefix(special_fragment_start) {
381                            return Ok(format!("crate{}::{}::{}", modules, name, fragment_name));
382                        }
383                    }
384                    return Ok(format!("crate{}::{}#{}", modules, name, fragment));
385                } else {
386                    return Ok(format!("crate{}::{}", modules, name));
387                }
388            }
389        }
390
391        Err(Error::UnrecognizedUrl(url.to_string()))
392    }
393}
394
395fn is_code_block_end(line: &str, backtick_count: usize) -> bool {
396    if line.len() < backtick_count {
397        false
398    } else {
399        for n in 0..backtick_count {
400            if line.as_bytes()[n] != b'`' {
401                return false;
402            }
403        }
404        true
405    }
406}
407
408fn is_code_block_start(line: &str) -> Option<usize> {
409    let mut backtick_count = 0;
410    while backtick_count < line.len() && line.as_bytes()[backtick_count] == b'`' {
411        backtick_count += 1;
412    }
413
414    if backtick_count >= 3 {
415        Some(backtick_count)
416    } else {
417        None
418    }
419}
420
421fn root_link(fragment: Option<&str>) -> String {
422    if let Some(fragment) = fragment {
423        format!("crate#{}", fragment)
424    } else {
425        "crate".to_owned()
426    }
427}
428
429// ======================================================================
430// TESTS
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    // ============================================================
437    // HELPERS
438
439    fn test(input: &str, expected: &str) {
440        assert_eq!(
441            rustdocify(input, "foo", None, Some("foo")),
442            Ok(expected.to_owned())
443        );
444    }
445
446    // ============================================================
447    // MISC
448
449    #[test]
450    fn retain_exact_newlines() {
451        let input = "## A\n## B\r\na\nb\r\n[x]: https://docs.rs/foo\n[y]: https://docs.rs/foo\r\n";
452        let expected = "# A\n# B\r\na\nb\r\n[x]: crate\n[y]: crate\r\n";
453        assert_eq!(rustdocify(input, "foo", None, None).unwrap(), expected);
454    }
455
456    // ============================================================
457    // HEADERS - ERRORS
458
459    #[test]
460    fn non_first_top_level_header() {
461        assert_eq!(
462            rustdocify("## Foo\n# Bar", "foo", None, None),
463            Err(Error::NonFirstTopLevelHeader("# Bar".to_owned()))
464        );
465    }
466
467    // ============================================================
468    // HEADERS - IGNORE
469
470    #[test]
471    fn ignore_headers_without_space_before() {
472        test("#Foo\n##Bar", "#Foo\n##Bar");
473    }
474
475    // ============================================================
476    // HEADERS
477
478    #[test]
479    fn convert_headers() {
480        test(
481            "# Foo\nfoo\n## Bar\nbar\n### Baz",
482            "foo\n# Bar\nbar\n## Baz",
483        );
484    }
485
486    // ============================================================
487    // LINKS - ERRORS
488
489    #[test]
490    fn missing_version_in_url() {
491        assert_eq!(
492            rustdocify("[x]: https://docs.rs/foo", "foo", Some("0.1.0"), None),
493            Err(Error::MissingVersionInUrl("https://docs.rs/foo".to_owned()))
494        );
495    }
496
497    #[test]
498    fn unrecognized_url_html() {
499        let input = "[x]: https://docs.rs/foo/*/foo/hello_world.html";
500        let expected = "https://docs.rs/foo/*/foo/hello_world.html".to_owned();
501        assert_eq!(
502            rustdocify(input, "foo", None, None),
503            Err(Error::UnrecognizedUrl(expected))
504        );
505    }
506
507    #[test]
508    fn unrecognized_url_non_html() {
509        let input = "[x]: https://docs.rs/foo/*/foo/hello_world.png";
510        let expected = "https://docs.rs/foo/*/foo/hello_world.png".to_owned();
511        assert_eq!(
512            rustdocify(input, "foo", None, None),
513            Err(Error::UnrecognizedUrl(expected))
514        );
515    }
516
517    #[test]
518    fn wrong_crate_name_in_url() {
519        assert_eq!(
520            rustdocify("[x]: https://docs.rs/foo/*/bar", "foo", None, Some("foo")),
521            Err(Error::WrongCrateNameInUrl(
522                "https://docs.rs/foo/*/bar".to_owned()
523            ))
524        );
525    }
526
527    #[test]
528    fn wrong_version_in_url() {
529        assert_eq!(
530            rustdocify("[x]: https://docs.rs/foo/0.2.0", "foo", Some("0.1.0"), None),
531            Err(Error::WrongVersionInUrl(
532                "https://docs.rs/foo/0.2.0".to_owned()
533            ))
534        );
535    }
536
537    // ============================================================
538    // LINKS - NOT LINK LINE
539
540    #[test]
541    fn no_close_bracket() {
542        test("[x: https://docs.rs/foo", "[x: https://docs.rs/foo");
543    }
544
545    #[test]
546    fn no_colon_after_close_bracket() {
547        test("[x] https://docs.rs/foo", "[x] https://docs.rs/foo");
548    }
549
550    #[test]
551    fn no_space_after_colon() {
552        test("[x]:https://docs.rs/foo", "[x]:https://docs.rs/foo");
553    }
554
555    #[test]
556    fn no_url() {
557        test("[x]:   ", "[x]:   ");
558    }
559
560    // ============================================================
561    // LINKS - IGNORE
562
563    #[test]
564    fn ignore_link_of_another_domain() {
565        test(
566            "[x]: https://example.com/foo",
567            "[x]: https://example.com/foo",
568        );
569    }
570
571    #[test]
572    fn ignore_link_to_another_package() {
573        test("[x]: https://docs.rs/bar", "[x]: https://docs.rs/bar");
574    }
575
576    #[test]
577    fn ignore_link_to_another_package_which_has_this_package_as_prefix() {
578        test("[x]: https://docs.rs/foobar", "[x]: https://docs.rs/foobar");
579    }
580
581    // ============================================================
582    // LINKS - MISC
583
584    #[test]
585    fn retain_multiple_whitespace_before_url() {
586        test("[x]:   https://docs.rs/foo", "[x]:   crate");
587    }
588
589    #[test]
590    fn retain_stuff_after_url() {
591        test("[x]: https://docs.rs/foo   hello", "[x]: crate   hello");
592    }
593
594    // ============================================================
595    // LINKS - NO VERSION
596
597    #[test]
598    fn package() {
599        test("[x]: https://docs.rs/foo", "[x]: crate");
600    }
601
602    #[test]
603    fn package_fragment() {
604        test("[x]: https://docs.rs/foo#fragment", "[x]: crate#fragment");
605    }
606
607    #[test]
608    fn package_slash() {
609        test("[x]: https://docs.rs/foo/", "[x]: crate");
610    }
611
612    #[test]
613    fn package_slash_fragment() {
614        test("[x]: https://docs.rs/foo/#fragment", "[x]: crate#fragment");
615    }
616
617    // ============================================================
618    // LINKS - HAS VERSION, NO CRATE
619
620    #[test]
621    fn package_version() {
622        test("[x]: https://docs.rs/foo/*", "[x]: crate");
623    }
624
625    #[test]
626    fn package_version_fragment() {
627        test("[x]: https://docs.rs/foo/*#fragment", "[x]: crate#fragment");
628    }
629
630    #[test]
631    fn package_version_index() {
632        test("[x]: https://docs.rs/foo/*/index.html", "[x]: crate");
633    }
634
635    #[test]
636    fn package_version_index_fragment() {
637        test(
638            "[x]: https://docs.rs/foo/*/index.html#fragment",
639            "[x]: crate#fragment",
640        );
641    }
642
643    #[test]
644    fn package_version_slash() {
645        test("[x]: https://docs.rs/foo/*/", "[x]: crate");
646    }
647
648    #[test]
649    fn package_version_slash_fragment() {
650        test(
651            "[x]: https://docs.rs/foo/*/#fragment",
652            "[x]: crate#fragment",
653        );
654    }
655
656    // ============================================================
657    // LINKS - HAS CRATE, BUT NO MORE SEGMENTS
658
659    #[test]
660    fn package_version_crate() {
661        test("[x]: https://docs.rs/foo/*/foo", "[x]: crate");
662    }
663
664    #[test]
665    fn package_version_crate_fragment() {
666        test(
667            "[x]: https://docs.rs/foo/*/foo#fragment",
668            "[x]: crate#fragment",
669        );
670    }
671
672    #[test]
673    fn package_version_crate_index() {
674        test("[x]: https://docs.rs/foo/*/foo/index.html", "[x]: crate");
675    }
676
677    #[test]
678    fn package_version_crate_index_fragment() {
679        test(
680            "[x]: https://docs.rs/foo/*/foo/index.html#fragment",
681            "[x]: crate#fragment",
682        );
683    }
684
685    #[test]
686    fn package_version_crate_slash() {
687        test("[x]: https://docs.rs/foo/*/foo/", "[x]: crate");
688    }
689
690    #[test]
691    fn package_version_crate_slash_fragment() {
692        test(
693            "[x]: https://docs.rs/foo/*/foo/#fragment",
694            "[x]: crate#fragment",
695        );
696    }
697
698    // ============================================================
699    // LINKS - MODULE
700
701    #[test]
702    fn module() {
703        test("[x]: https://docs.rs/foo/*/foo/a/b", "[x]: crate::a::b");
704    }
705
706    #[test]
707    fn module_fragment() {
708        test(
709            "[x]: https://docs.rs/foo/*/foo/a/b#fragment",
710            "[x]: crate::a::b#fragment",
711        );
712    }
713
714    #[test]
715    fn module_index() {
716        test(
717            "[x]: https://docs.rs/foo/*/foo/a/b/index.html",
718            "[x]: crate::a::b",
719        );
720    }
721
722    #[test]
723    fn module_index_fragment() {
724        test(
725            "[x]: https://docs.rs/foo/*/foo/a/b/index.html#fragment",
726            "[x]: crate::a::b#fragment",
727        );
728    }
729
730    #[test]
731    fn module_slash() {
732        test("[x]: https://docs.rs/foo/*/foo/a/b/", "[x]: crate::a::b");
733    }
734
735    #[test]
736    fn module_slash_fragment() {
737        test(
738            "[x]: https://docs.rs/foo/*/foo/a/b/#fragment",
739            "[x]: crate::a::b#fragment",
740        );
741    }
742
743    // ============================================================
744    // LINKS - ENUM
745
746    #[test]
747    fn root_enum() {
748        test(
749            "[x]: https://docs.rs/foo/*/foo/enum.Foo.html",
750            "[x]: crate::Foo",
751        );
752    }
753
754    #[test]
755    fn root_enum_fragment() {
756        test(
757            "[x]: https://docs.rs/foo/*/foo/enum.Foo.html#fragment",
758            "[x]: crate::Foo#fragment",
759        );
760    }
761
762    #[test]
763    fn root_enum_method() {
764        test(
765            "[x]: https://docs.rs/foo/*/foo/enum.Foo.html#method.bar",
766            "[x]: crate::Foo::bar",
767        );
768    }
769
770    #[test]
771    fn root_enum_variant() {
772        test(
773            "[x]: https://docs.rs/foo/*/foo/enum.Foo.html#variant.Bar",
774            "[x]: crate::Foo::Bar",
775        );
776    }
777
778    #[test]
779    fn module_enum() {
780        test(
781            "[x]: https://docs.rs/foo/*/foo/a/b/enum.Foo.html",
782            "[x]: crate::a::b::Foo",
783        );
784    }
785
786    #[test]
787    fn module_enum_fragment() {
788        test(
789            "[x]: https://docs.rs/foo/*/foo/a/b/enum.Foo.html#fragment",
790            "[x]: crate::a::b::Foo#fragment",
791        );
792    }
793
794    #[test]
795    fn module_enum_method() {
796        test(
797            "[x]: https://docs.rs/foo/*/foo/a/b/enum.Foo.html#method.bar",
798            "[x]: crate::a::b::Foo::bar",
799        );
800    }
801
802    #[test]
803    fn module_enum_variant() {
804        test(
805            "[x]: https://docs.rs/foo/*/foo/a/b/enum.Foo.html#variant.Bar",
806            "[x]: crate::a::b::Foo::Bar",
807        );
808    }
809
810    // ============================================================
811    // LINKS - FUNCTION
812
813    #[test]
814    fn root_function() {
815        test(
816            "[x]: https://docs.rs/foo/*/foo/fn.bar.html",
817            "[x]: crate::bar",
818        );
819    }
820
821    #[test]
822    fn root_function_fragment() {
823        test(
824            "[x]: https://docs.rs/foo/*/foo/fn.bar.html#fragment",
825            "[x]: crate::bar#fragment",
826        );
827    }
828
829    #[test]
830    fn module_function() {
831        test(
832            "[x]: https://docs.rs/foo/*/foo/a/b/fn.bar.html",
833            "[x]: crate::a::b::bar",
834        );
835    }
836
837    #[test]
838    fn module_function_fragment() {
839        test(
840            "[x]: https://docs.rs/foo/*/foo/a/b/fn.bar.html#fragment",
841            "[x]: crate::a::b::bar#fragment",
842        );
843    }
844
845    // ============================================================
846    // LINKS - MACRO
847
848    #[test]
849    fn root_macro() {
850        test(
851            "[x]: https://docs.rs/foo/*/foo/macro.bar.html",
852            "[x]: crate::bar",
853        );
854    }
855
856    #[test]
857    fn root_macro_fragment() {
858        test(
859            "[x]: https://docs.rs/foo/*/foo/macro.bar.html#fragment",
860            "[x]: crate::bar#fragment",
861        );
862    }
863
864    #[test]
865    fn module_macro() {
866        test(
867            "[x]: https://docs.rs/foo/*/foo/a/b/macro.bar.html",
868            "[x]: crate::a::b::bar",
869        );
870    }
871
872    #[test]
873    fn module_macro_fragment() {
874        test(
875            "[x]: https://docs.rs/foo/*/foo/a/b/macro.bar.html#fragment",
876            "[x]: crate::a::b::bar#fragment",
877        );
878    }
879
880    // ============================================================
881    // LINKS - STRUCT
882
883    #[test]
884    fn root_struct() {
885        test(
886            "[x]: https://docs.rs/foo/*/foo/struct.Foo.html",
887            "[x]: crate::Foo",
888        );
889    }
890
891    #[test]
892    fn root_struct_fragment() {
893        test(
894            "[x]: https://docs.rs/foo/*/foo/struct.Foo.html#fragment",
895            "[x]: crate::Foo#fragment",
896        );
897    }
898
899    #[test]
900    fn root_struct_method() {
901        test(
902            "[x]: https://docs.rs/foo/*/foo/struct.Foo.html#method.bar",
903            "[x]: crate::Foo::bar",
904        );
905    }
906
907    #[test]
908    fn module_struct() {
909        test(
910            "[x]: https://docs.rs/foo/*/foo/a/b/struct.Foo.html",
911            "[x]: crate::a::b::Foo",
912        );
913    }
914
915    #[test]
916    fn module_struct_fragment() {
917        test(
918            "[x]: https://docs.rs/foo/*/foo/a/b/struct.Foo.html#fragment",
919            "[x]: crate::a::b::Foo#fragment",
920        );
921    }
922
923    #[test]
924    fn module_struct_method() {
925        test(
926            "[x]: https://docs.rs/foo/*/foo/a/b/struct.Foo.html#method.bar",
927            "[x]: crate::a::b::Foo::bar",
928        );
929    }
930
931    // ============================================================
932    // LINKS - TRAIT
933
934    #[test]
935    fn root_trait() {
936        test(
937            "[x]: https://docs.rs/foo/*/foo/trait.Foo.html",
938            "[x]: crate::Foo",
939        );
940    }
941
942    #[test]
943    fn root_trait_fragment() {
944        test(
945            "[x]: https://docs.rs/foo/*/foo/trait.Foo.html#fragment",
946            "[x]: crate::Foo#fragment",
947        );
948    }
949
950    #[test]
951    fn root_trait_method() {
952        test(
953            "[x]: https://docs.rs/foo/*/foo/trait.Foo.html#tymethod.bar",
954            "[x]: crate::Foo::bar",
955        );
956    }
957
958    #[test]
959    fn module_trait() {
960        test(
961            "[x]: https://docs.rs/foo/*/foo/a/b/trait.Foo.html",
962            "[x]: crate::a::b::Foo",
963        );
964    }
965
966    #[test]
967    fn module_trait_fragment() {
968        test(
969            "[x]: https://docs.rs/foo/*/foo/a/b/trait.Foo.html#fragment",
970            "[x]: crate::a::b::Foo#fragment",
971        );
972    }
973
974    #[test]
975    fn module_trait_method() {
976        test(
977            "[x]: https://docs.rs/foo/*/foo/a/b/trait.Foo.html#tymethod.bar",
978            "[x]: crate::a::b::Foo::bar",
979        );
980    }
981
982    // ============================================================
983    // CODE BLOCKS
984
985    #[test]
986    fn two_backticks_doesnt_begin_code_block() {
987        test("``\n## a", "``\n# a");
988    }
989
990    #[test]
991    fn three_backtick_code_block_with_header() {
992        test("```\n## a\n```\n## b", "```\n## a\n```\n# b");
993    }
994
995    #[test]
996    fn four_backtick_code_block_with_header() {
997        test("````\n```\n## a\n````\n## b", "````\n```\n## a\n````\n# b");
998    }
999
1000    #[test]
1001    fn code_block_with_link() {
1002        test(
1003            "```\n[a]: https://docs.rs/foo\n```\n[b]: https://docs.rs/foo",
1004            "```\n[a]: https://docs.rs/foo\n```\n[b]: crate",
1005        );
1006    }
1007
1008    #[test]
1009    fn code_block_with_short_line() {
1010        test("```\n\n```", "```\n\n```");
1011    }
1012
1013    #[test]
1014    fn code_block_with_type() {
1015        test("```foo\n## a\n```\n## b", "```foo\n## a\n```\n# b");
1016    }
1017}