Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix intra doc links not generated inside footnote definitions #134432

Merged
merged 2 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 50 additions & 16 deletions src/librustdoc/html/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,35 +344,48 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for CodeBlocks<'_, 'a, I> {
}

/// Make headings links with anchor IDs and build up TOC.
struct LinkReplacer<'a, I: Iterator<Item = Event<'a>>> {
inner: I,
struct LinkReplacerInner<'a> {
links: &'a [RenderedLink],
shortcut_link: Option<&'a RenderedLink>,
}

struct LinkReplacer<'a, I: Iterator<Item = Event<'a>>> {
iter: I,
inner: LinkReplacerInner<'a>,
}

impl<'a, I: Iterator<Item = Event<'a>>> LinkReplacer<'a, I> {
fn new(iter: I, links: &'a [RenderedLink]) -> Self {
LinkReplacer { inner: iter, links, shortcut_link: None }
LinkReplacer { iter, inner: { LinkReplacerInner { links, shortcut_link: None } } }
}
}

impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
type Item = Event<'a>;
// FIXME: Once we have specialized trait impl (for `Iterator` impl on `LinkReplacer`),
// we can remove this type and move back `LinkReplacerInner` fields into `LinkReplacer`.
struct SpannedLinkReplacer<'a, I: Iterator<Item = SpannedEvent<'a>>> {
iter: I,
inner: LinkReplacerInner<'a>,
}

fn next(&mut self) -> Option<Self::Item> {
let mut event = self.inner.next();
impl<'a, I: Iterator<Item = SpannedEvent<'a>>> SpannedLinkReplacer<'a, I> {
fn new(iter: I, links: &'a [RenderedLink]) -> Self {
SpannedLinkReplacer { iter, inner: { LinkReplacerInner { links, shortcut_link: None } } }
}
}

impl<'a> LinkReplacerInner<'a> {
fn handle_event(&mut self, event: &mut Event<'a>) {
// Replace intra-doc links and remove disambiguators from shortcut links (`[fn@f]`).
match &mut event {
match event {
// This is a shortcut link that was resolved by the broken_link_callback: `[fn@f]`
// Remove any disambiguator.
Some(Event::Start(Tag::Link {
Event::Start(Tag::Link {
// [fn@f] or [fn@f][]
link_type: LinkType::ShortcutUnknown | LinkType::CollapsedUnknown,
dest_url,
title,
..
})) => {
}) => {
debug!("saw start of shortcut link to {dest_url} with title {title}");
// If this is a shortcut link, it was resolved by the broken_link_callback.
// So the URL will already be updated properly.
Expand All @@ -389,13 +402,13 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
}
}
// Now that we're done with the shortcut link, don't replace any more text.
Some(Event::End(TagEnd::Link)) if self.shortcut_link.is_some() => {
Event::End(TagEnd::Link) if self.shortcut_link.is_some() => {
debug!("saw end of shortcut link");
self.shortcut_link = None;
}
// Handle backticks in inline code blocks, but only if we're in the middle of a shortcut link.
// [`fn@f`]
Some(Event::Code(text)) => {
Event::Code(text) => {
trace!("saw code {text}");
if let Some(link) = self.shortcut_link {
// NOTE: this only replaces if the code block is the *entire* text.
Expand All @@ -418,7 +431,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
}
// Replace plain text in links, but only in the middle of a shortcut link.
// [fn@f]
Some(Event::Text(text)) => {
Event::Text(text) => {
trace!("saw text {text}");
if let Some(link) = self.shortcut_link {
// NOTE: same limitations as `Event::Code`
Expand All @@ -434,7 +447,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
}
// If this is a link, but not a shortcut link,
// replace the URL, since the broken_link_callback was not called.
Some(Event::Start(Tag::Link { dest_url, title, .. })) => {
Event::Start(Tag::Link { dest_url, title, .. }) => {
if let Some(link) =
self.links.iter().find(|&link| *link.original_text == **dest_url)
{
Expand All @@ -447,12 +460,33 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
// Anything else couldn't have been a valid Rust path, so no need to replace the text.
_ => {}
}
}
}

impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
type Item = Event<'a>;

fn next(&mut self) -> Option<Self::Item> {
let mut event = self.iter.next();
if let Some(ref mut event) = event {
self.inner.handle_event(event);
}
// Yield the modified event
event
}
}

impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for SpannedLinkReplacer<'a, I> {
type Item = SpannedEvent<'a>;

fn next(&mut self) -> Option<Self::Item> {
let Some((mut event, range)) = self.iter.next() else { return None };
self.inner.handle_event(&mut event);
// Yield the modified event
Some((event, range))
}
}

/// Wrap HTML tables into `<div>` to prevent having the doc blocks width being too big.
struct TableWrapper<'a, I: Iterator<Item = Event<'a>>> {
inner: I,
Expand Down Expand Up @@ -1339,9 +1373,9 @@ 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 = footnotes::Footnotes::new(p, existing_footnotes);
let p = LinkReplacer::new(p.map(|(ev, _)| ev), links);
let p = TableWrapper::new(p);
let p = TableWrapper::new(p.map(|(ev, _)| ev));
CodeBlocks::new(p, codes, edition, playground)
})
}
Expand Down
24 changes: 24 additions & 0 deletions tests/rustdoc/intra-doc/link-in-footnotes-132208.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Rustdoc has multiple passes and if the footnote pass is run before the link replacer
// one, intra doc links are not generated inside footnote definitions. This test
// therefore ensures that intra-doc link are correctly generated inside footnote
// definitions.
//
// Regression test for <https://github.com/rust-lang/rust/issues/132208>.

#![crate_name = "foo"]

//@ has 'foo/index.html'
//@ has - '//*[@class="docblock"]//a[@href="struct.Bar.html"]' 'a'
//@ has - '//*[@class="docblock"]//*[@class="footnotes"]//a[@href="struct.Foo.html"]' 'b'

//! [a]: crate::Bar
//! [b]: crate::Foo
//!
//! link in body: [a]
//!
//! see footnote[^1]
//!
//! [^1]: link in footnote: [b]

pub struct Bar;
pub struct Foo;
Loading