diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 7e835585b73e8..a59738ce42a66 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -589,6 +589,109 @@ impl<'a, I: Iterator>> Iterator for HeadingLinks<'a, '_, } } +/// Type used to merge `` tags. It also takes into accounts links if the link only contains +/// `` tag(s). +/// +/// So if you have: +/// +/// `<`[`Stream`](crate::Foo)`>` +/// +/// Instead of getting: +/// +/// ```html +/// <Stream> +/// ``` +/// +/// You will get: +/// +/// ```html +/// <Stream> +/// ``` +struct CodeMerger<'a, I> { + inner: I, + queue: VecDeque>, +} + +impl<'a, I> CodeMerger<'a, I> { + fn new(iter: I) -> Self { + Self { inner: iter, queue: VecDeque::new() } + } + + fn merge_codes(&mut self, mut codes: VecDeque>) { + if codes.len() < 2 { + for item in codes.drain(..) { + self.queue.push_back(item); + } + return; + } + self.queue.push_back((Event::InlineHtml(CowStr::Borrowed("")), 0..0)); + for item in codes.drain(..) { + self.queue.push_back(match item { + (Event::Code(code), range) => (Event::Text(code), range), + _ => item, + }); + } + self.queue.push_back((Event::InlineHtml(CowStr::Borrowed("")), 0..0)); + } +} + +impl<'a, I: Iterator>> Iterator for CodeMerger<'a, I> { + type Item = SpannedEvent<'a>; + + fn next(&mut self) -> Option { + if let Some(item) = self.queue.pop_front() { + return Some(item); + } + + let mut buffed_link_items = VecDeque::new(); + let mut codes = VecDeque::new(); + let mut is_inside_link = false; + let mut extra = None; + + while let Some(event) = self.inner.next() { + match event.0 { + Event::Code(_) => { + if is_inside_link { + buffed_link_items.push_back(event); + } else { + codes.push_back(event); + } + } + Event::Start(Tag::Link { .. }) => { + buffed_link_items.push_back(event); + is_inside_link = true; + } + Event::End(TagEnd::Link { .. }) if is_inside_link => { + // Seems like it's all good! + for item in buffed_link_items.drain(..) { + codes.push_back(item); + } + codes.push_back(event); + is_inside_link = false; + } + _ => { + extra = Some(event); + break; + } + } + } + // First we merge the code items if any. + self.merge_codes(codes); + // The link doesn't only contain codes so we first merge the codes into 1, + // then we put the link elements into the queue as well. + if is_inside_link { + for item in buffed_link_items.drain(..) { + self.queue.push_back(item); + } + } + if let Some(item) = extra { + self.queue.push_back(item); + } + + self.queue.pop_front() + } +} + /// Extracts just the first paragraph. struct SummaryLine<'a, I: Iterator>> { inner: I, @@ -1374,6 +1477,7 @@ impl<'a> Markdown<'a> { ids.handle_footnotes(|ids, existing_footnotes| { let p = HeadingLinks::new(p, None, ids, heading_offset); let p = SpannedLinkReplacer::new(p, links); + let p = CodeMerger::new(p); let p = footnotes::Footnotes::new(p, existing_footnotes); let p = TableWrapper::new(p.map(|(ev, _)| ev)); CodeBlocks::new(p, codes, edition, playground) @@ -1455,6 +1559,7 @@ impl MarkdownWithToc<'_> { ids.handle_footnotes(|ids, existing_footnotes| { let p = HeadingLinks::new(p, Some(&mut toc), ids, HeadingOffset::H1); + let p = CodeMerger::new(p); let p = footnotes::Footnotes::new(p, existing_footnotes); let p = TableWrapper::new(p.map(|(ev, _)| ev)); let p = CodeBlocks::new(p, codes, edition, playground); @@ -1489,6 +1594,7 @@ impl MarkdownItemInfo<'_> { ids.handle_footnotes(|ids, existing_footnotes| { let p = HeadingLinks::new(p, None, ids, HeadingOffset::H1); + let p = CodeMerger::new(p); let p = footnotes::Footnotes::new(p, existing_footnotes); let p = TableWrapper::new(p.map(|(ev, _)| ev)); let p = p.filter(|event| { @@ -1516,9 +1622,9 @@ impl MarkdownSummaryLine<'_> { .map(|link| (link.href.as_str().into(), link.tooltip.as_str().into())) }; - let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer)) - .peekable(); - let mut summary = SummaryLine::new(p); + let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer)); + let p = CodeMerger::new(p.map(|ev| (ev, 0..0))); + let mut summary = SummaryLine::new(p.map(|(ev, _)| ev).peekable()); let mut s = String::new(); diff --git a/tests/rustdoc-gui/docblock-big-code-mobile.goml b/tests/rustdoc-gui/docblock-big-code-mobile.goml index 71e08e2c7e260..abaa00486637b 100644 --- a/tests/rustdoc-gui/docblock-big-code-mobile.goml +++ b/tests/rustdoc-gui/docblock-big-code-mobile.goml @@ -12,4 +12,5 @@ assert-size: (".docblock p > code", {"height": 48}) // Same check, but where the long code block is also a link go-to: "file://" + |DOC_PATH| + "/test_docs/long_code_block_link/index.html" -assert-size: (".docblock p > a > code", {"height": 48}) +assert-size: (".docblock p > code", {"height": 48}) +assert-size: (".docblock p > code > a", {"height": 44}) diff --git a/tests/rustdoc/code-tags-merge.rs b/tests/rustdoc/code-tags-merge.rs new file mode 100644 index 0000000000000..6c6a6daea2fff --- /dev/null +++ b/tests/rustdoc/code-tags-merge.rs @@ -0,0 +1,28 @@ +// This test checks that rustdoc is combining `` tags as expected. + +#![crate_name = "foo"] + +//@ has 'foo/index.html' + +// First we check that summary lines also get the `` merge. +//@ has - '//dd/code' 'Stream>' +//@ !has - '//dd/code/code' 'Stream>' +// Then we check that the docblocks have it too. +//@ has 'foo/struct.Foo.html' +//@ has - '//*[@class="docblock"]//code' 'Stream>' +//@ has - '//*[@class="docblock"]//code' 'Stream>' +/// [`Stream`](crate::Foo)`>` +pub struct Foo; + +impl Foo { + //@ has - '//*[@class="impl-items"]//*[@class="docblock"]//code' '' + /// A `<`[`Stream`](crate::Foo)`>` stuff. + pub fn bar() {} + + //@ has - '//*[@class="impl-items"]//*[@class="docblock"]//code' '<' + //@ has - '//*[@class="impl-items"]//*[@class="docblock"]//a' 'Stream a' + //@ has - '//*[@class="impl-items"]//*[@class="docblock"]//code' 'Stream' + //@ has - '//*[@class="impl-items"]//*[@class="docblock"]//code' '>' + /// A `<`[`Stream` a](crate::Foo)`>` stuff. + pub fn foo() {} +} diff --git a/tests/rustdoc/intra-doc/disambiguators-removed.rs b/tests/rustdoc/intra-doc/disambiguators-removed.rs index 613156222ed0e..1b3290d1c3768 100644 --- a/tests/rustdoc/intra-doc/disambiguators-removed.rs +++ b/tests/rustdoc/intra-doc/disambiguators-removed.rs @@ -2,15 +2,15 @@ // first try backticks /// Trait: [`trait@Name`], fn: [`fn@Name`], [`Name`][`macro@Name`] //@ has disambiguators_removed/struct.AtDisambiguator.html -//@ has - '//a[@href="trait.Name.html"][code]' "Name" -//@ has - '//a[@href="fn.Name.html"][code]' "Name" -//@ has - '//a[@href="macro.Name.html"][code]' "Name" +//@ has - '//code/a[@href="trait.Name.html"]' "Name" +//@ has - '//code/a[@href="fn.Name.html"]' "Name" +//@ has - '//code/a[@href="macro.Name.html"]' "Name" pub struct AtDisambiguator; /// fn: [`Name()`], macro: [`Name!`] //@ has disambiguators_removed/struct.SymbolDisambiguator.html -//@ has - '//a[@href="fn.Name.html"][code]' "Name()" -//@ has - '//a[@href="macro.Name.html"][code]' "Name!" +//@ has - '//code/a[@href="fn.Name.html"]' "Name()" +//@ has - '//code/a[@href="macro.Name.html"]' "Name!" pub struct SymbolDisambiguator; // Now make sure that backticks aren't added if they weren't already there diff --git a/tests/rustdoc/primitive-link.rs b/tests/rustdoc/primitive-link.rs index 3fe9cdc3ca702..6d620e63199a1 100644 --- a/tests/rustdoc/primitive-link.rs +++ b/tests/rustdoc/primitive-link.rs @@ -1,12 +1,11 @@ #![crate_name = "foo"] - -//@ has foo/struct.Foo.html '//*[@class="docblock"]/p/a[@href="{{channel}}/std/primitive.u32.html"]' 'u32' +//@ has foo/struct.Foo.html '//*[@class="docblock"]/p/code/a[@href="{{channel}}/std/primitive.u32.html"]' 'u32' //@ has foo/struct.Foo.html '//*[@class="docblock"]/p/a[@href="{{channel}}/std/primitive.i64.html"]' 'i64' //@ has foo/struct.Foo.html '//*[@class="docblock"]/p/a[@href="{{channel}}/std/primitive.i32.html"]' 'std::primitive::i32' //@ has foo/struct.Foo.html '//*[@class="docblock"]/p/a[@href="{{channel}}/std/primitive.str.html"]' 'std::primitive::str' -//@ has foo/struct.Foo.html '//*[@class="docblock"]/p/a[@href="{{channel}}/std/primitive.i32.html#associatedconstant.MAX"]' 'std::primitive::i32::MAX' +//@ has foo/struct.Foo.html '//*[@class="docblock"]/p/code/a[@href="{{channel}}/std/primitive.i32.html#associatedconstant.MAX"]' 'std::primitive::i32::MAX' /// It contains [`u32`] and [i64]. /// It also links to [std::primitive::i32], [std::primitive::str], diff --git a/tests/rustdoc/short-docblock.rs b/tests/rustdoc/short-docblock.rs index fa0af85696ac4..ee8d7ce43d5e2 100644 --- a/tests/rustdoc/short-docblock.rs +++ b/tests/rustdoc/short-docblock.rs @@ -20,7 +20,7 @@ pub fn foo() {} /// foo mod pub mod foo {} -//@ has foo/index.html '//dd/a[@href="https://nougat.world"]/code' 'nougat' +//@ has foo/index.html '//dd/code/a[@href="https://nougat.world"]' 'nougat' /// [`nougat`](https://nougat.world) pub struct Bar;