1use std::fmt;
2
3#[derive(Clone, Debug, PartialEq)]
8pub enum Error {
9 MissingVersionInUrl(String),
17
18 NonFirstTopLevelHeader(String),
28
29 UnrecognizedUrl(String),
39
40 WrongCrateNameInUrl(String),
48
49 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
87pub 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 if is_code_block_end(line, level) {
118 code_block_level = None;
119 }
120
121 result.push_str(line);
122 } else {
123 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
142fn 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
177fn 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 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 if path.is_empty() {
278 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 if path.len() == 1 {
302 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 if path.len() == 2 {
322 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 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#[cfg(test)]
433mod tests {
434 use super::*;
435
436 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 #[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 #[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 #[test]
471 fn ignore_headers_without_space_before() {
472 test("#Foo\n##Bar", "#Foo\n##Bar");
473 }
474
475 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}