diff options
| author | Vadim Petrochenkov <vadim.petrochenkov@gmail.com> | 2022-04-21 15:01:44 +0300 |
|---|---|---|
| committer | Vadim Petrochenkov <vadim.petrochenkov@gmail.com> | 2022-04-21 16:13:02 +0300 |
| commit | 7803a411517f0f1e64d8f37ce377c684d1f2d1fd (patch) | |
| tree | d90e88cd5adf93d13bd937f80d043cf0a980ca7c /src/librustdoc/clean | |
| parent | 3d3dafb771fb714baeb36793f2d4cd333a8e9c34 (diff) | |
| download | rust-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.rs | 89 | ||||
| -rw-r--r-- | src/librustdoc/clean/types/tests.rs | 70 |
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"); +} |
