about summary refs log tree commit diff
path: root/src/librustdoc/clean
diff options
context:
space:
mode:
authorVadim Petrochenkov <vadim.petrochenkov@gmail.com>2022-04-21 15:01:44 +0300
committerVadim Petrochenkov <vadim.petrochenkov@gmail.com>2022-04-21 16:13:02 +0300
commit7803a411517f0f1e64d8f37ce377c684d1f2d1fd (patch)
treed90e88cd5adf93d13bd937f80d043cf0a980ca7c /src/librustdoc/clean
parent3d3dafb771fb714baeb36793f2d4cd333a8e9c34 (diff)
downloadrust-7803a411517f0f1e64d8f37ce377c684d1f2d1fd.tar.gz
rust-7803a411517f0f1e64d8f37ce377c684d1f2d1fd.zip
rustdoc: Unindent doc fragments on `Attributes` construction
Diffstat (limited to 'src/librustdoc/clean')
-rw-r--r--src/librustdoc/clean/types.rs89
-rw-r--r--src/librustdoc/clean/types/tests.rs70
2 files changed, 156 insertions, 3 deletions
diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index 95ac3ab622a..2b65b8f910c 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -1,13 +1,11 @@
 use std::cell::RefCell;
 use std::default::Default;
-use std::fmt;
 use std::hash::Hash;
-use std::iter;
 use std::lazy::SyncOnceCell as OnceCell;
 use std::path::PathBuf;
 use std::rc::Rc;
 use std::sync::Arc;
-use std::vec;
+use std::{cmp, fmt, iter};
 
 use arrayvec::ArrayVec;
 
@@ -55,6 +53,9 @@ crate use self::Type::{
 };
 crate use self::Visibility::{Inherited, Public};
 
+#[cfg(test)]
+mod tests;
+
 crate type ItemIdSet = FxHashSet<ItemId>;
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
@@ -1028,6 +1029,86 @@ crate fn collapse_doc_fragments(doc_strings: &[DocFragment]) -> String {
     acc
 }
 
+/// Removes excess indentation on comments in order for the Markdown
+/// to be parsed correctly. This is necessary because the convention for
+/// writing documentation is to provide a space between the /// or //! marker
+/// and the doc text, but Markdown is whitespace-sensitive. For example,
+/// a block of text with four-space indentation is parsed as a code block,
+/// so if we didn't unindent comments, these list items
+///
+/// /// A list:
+/// ///
+/// ///    - Foo
+/// ///    - Bar
+///
+/// would be parsed as if they were in a code block, which is likely not what the user intended.
+fn unindent_doc_fragments(docs: &mut Vec<DocFragment>) {
+    // `add` is used in case the most common sugared doc syntax is used ("/// "). The other
+    // fragments kind's lines are never starting with a whitespace unless they are using some
+    // markdown formatting requiring it. Therefore, if the doc block have a mix between the two,
+    // we need to take into account the fact that the minimum indent minus one (to take this
+    // whitespace into account).
+    //
+    // For example:
+    //
+    // /// hello!
+    // #[doc = "another"]
+    //
+    // In this case, you want "hello! another" and not "hello!  another".
+    let add = if docs.windows(2).any(|arr| arr[0].kind != arr[1].kind)
+        && docs.iter().any(|d| d.kind == DocFragmentKind::SugaredDoc)
+    {
+        // In case we have a mix of sugared doc comments and "raw" ones, we want the sugared one to
+        // "decide" how much the minimum indent will be.
+        1
+    } else {
+        0
+    };
+
+    // `min_indent` is used to know how much whitespaces from the start of each lines must be
+    // removed. Example:
+    //
+    // ///     hello!
+    // #[doc = "another"]
+    //
+    // In here, the `min_indent` is 1 (because non-sugared fragment are always counted with minimum
+    // 1 whitespace), meaning that "hello!" will be considered a codeblock because it starts with 4
+    // (5 - 1) whitespaces.
+    let Some(min_indent) = docs
+        .iter()
+        .map(|fragment| {
+            fragment.doc.as_str().lines().fold(usize::MAX, |min_indent, line| {
+                if line.chars().all(|c| c.is_whitespace()) {
+                    min_indent
+                } else {
+                    // Compare against either space or tab, ignoring whether they are
+                    // mixed or not.
+                    let whitespace = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
+                    cmp::min(min_indent, whitespace)
+                        + if fragment.kind == DocFragmentKind::SugaredDoc { 0 } else { add }
+                }
+            })
+        })
+        .min()
+    else {
+        return;
+    };
+
+    for fragment in docs {
+        if fragment.doc == kw::Empty {
+            continue;
+        }
+
+        let min_indent = if fragment.kind != DocFragmentKind::SugaredDoc && min_indent > 0 {
+            min_indent - add
+        } else {
+            min_indent
+        };
+
+        fragment.indent = min_indent;
+    }
+}
+
 /// A link that has not yet been rendered.
 ///
 /// This link will be turned into a rendered link by [`Item::links`].
@@ -1119,6 +1200,8 @@ impl Attributes {
             }
         }
 
+        unindent_doc_fragments(&mut doc_strings);
+
         Attributes { doc_strings, other_attrs }
     }
 
diff --git a/src/librustdoc/clean/types/tests.rs b/src/librustdoc/clean/types/tests.rs
new file mode 100644
index 00000000000..71eddf4348f
--- /dev/null
+++ b/src/librustdoc/clean/types/tests.rs
@@ -0,0 +1,70 @@
+use super::*;
+
+use crate::clean::collapse_doc_fragments;
+
+use rustc_span::create_default_session_globals_then;
+use rustc_span::source_map::DUMMY_SP;
+use rustc_span::symbol::Symbol;
+
+fn create_doc_fragment(s: &str) -> Vec<DocFragment> {
+    vec![DocFragment {
+        span: DUMMY_SP,
+        parent_module: None,
+        doc: Symbol::intern(s),
+        kind: DocFragmentKind::SugaredDoc,
+        indent: 0,
+    }]
+}
+
+#[track_caller]
+fn run_test(input: &str, expected: &str) {
+    create_default_session_globals_then(|| {
+        let mut s = create_doc_fragment(input);
+        unindent_doc_fragments(&mut s);
+        assert_eq!(collapse_doc_fragments(&s), expected);
+    });
+}
+
+#[test]
+fn should_unindent() {
+    run_test("    line1\n    line2", "line1\nline2");
+}
+
+#[test]
+fn should_unindent_multiple_paragraphs() {
+    run_test("    line1\n\n    line2", "line1\n\nline2");
+}
+
+#[test]
+fn should_leave_multiple_indent_levels() {
+    // Line 2 is indented another level beyond the
+    // base indentation and should be preserved
+    run_test("    line1\n\n        line2", "line1\n\n    line2");
+}
+
+#[test]
+fn should_ignore_first_line_indent() {
+    run_test("line1\n    line2", "line1\n    line2");
+}
+
+#[test]
+fn should_not_ignore_first_line_indent_in_a_single_line_para() {
+    run_test("line1\n\n    line2", "line1\n\n    line2");
+}
+
+#[test]
+fn should_unindent_tabs() {
+    run_test("\tline1\n\tline2", "line1\nline2");
+}
+
+#[test]
+fn should_trim_mixed_indentation() {
+    run_test("\t    line1\n\t    line2", "line1\nline2");
+    run_test("    \tline1\n    \tline2", "line1\nline2");
+}
+
+#[test]
+fn should_not_trim() {
+    run_test("\t    line1  \n\t    line2", "line1  \nline2");
+    run_test("    \tline1  \n    \tline2", "line1  \nline2");
+}