Skip to content

Commit 461c460

Browse files
committed
Auto merge of #30036 - mitaa:doc_id, r=alexcrichton
This expands the code which generates unique IDs for Markdown headers within a single block to each rendered page. fixes #25001 fixes #29449
2 parents 6941b25 + fb7008c commit 461c460

File tree

5 files changed

+203
-59
lines changed

5 files changed

+203
-59
lines changed

src/librustdoc/html/markdown.rs

+33-31
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
//! (bundled into the rust runtime). This module self-contains the C bindings
1515
//! and necessary legwork to render markdown, and exposes all of the
1616
//! functionality through a unit-struct, `Markdown`, which has an implementation
17-
//! of `fmt::String`. Example usage:
17+
//! of `fmt::Display`. Example usage:
1818
//!
1919
//! ```rust,ignore
2020
//! use rustdoc::html::markdown::Markdown;
@@ -29,19 +29,19 @@
2929
use libc;
3030
use std::ascii::AsciiExt;
3131
use std::cell::RefCell;
32-
use std::collections::HashMap;
3332
use std::default::Default;
3433
use std::ffi::CString;
3534
use std::fmt;
3635
use std::slice;
3736
use std::str;
3837

38+
use html::render::derive_id;
3939
use html::toc::TocBuilder;
4040
use html::highlight;
4141
use html::escape::Escape;
4242
use test;
4343

44-
/// A unit struct which has the `fmt::String` trait implemented. When
44+
/// A unit struct which has the `fmt::Display` trait implemented. When
4545
/// formatted, this struct will emit the HTML corresponding to the rendered
4646
/// version of the contained markdown string.
4747
pub struct Markdown<'a>(pub &'a str);
@@ -210,10 +210,6 @@ fn collapse_whitespace(s: &str) -> String {
210210
s.split_whitespace().collect::<Vec<_>>().join(" ")
211211
}
212212

213-
thread_local!(static USED_HEADER_MAP: RefCell<HashMap<String, usize>> = {
214-
RefCell::new(HashMap::new())
215-
});
216-
217213
thread_local!(pub static PLAYGROUND_KRATE: RefCell<Option<Option<String>>> = {
218214
RefCell::new(None)
219215
});
@@ -311,16 +307,7 @@ pub fn render(w: &mut fmt::Formatter, s: &str, print_toc: bool) -> fmt::Result {
311307
let opaque = unsafe { (*data).opaque as *mut hoedown_html_renderer_state };
312308
let opaque = unsafe { &mut *((*opaque).opaque as *mut MyOpaque) };
313309

314-
// Make sure our hyphenated ID is unique for this page
315-
let id = USED_HEADER_MAP.with(|map| {
316-
let id = match map.borrow_mut().get_mut(&id) {
317-
None => id,
318-
Some(a) => { *a += 1; format!("{}-{}", id, *a - 1) }
319-
};
320-
map.borrow_mut().insert(id.clone(), 1);
321-
id
322-
});
323-
310+
let id = derive_id(id);
324311

325312
let sec = opaque.toc_builder.as_mut().map_or("".to_owned(), |builder| {
326313
format!("{} ", builder.push(level as u32, s.clone(), id.clone()))
@@ -335,8 +322,6 @@ pub fn render(w: &mut fmt::Formatter, s: &str, print_toc: bool) -> fmt::Result {
335322
unsafe { hoedown_buffer_puts(ob, text.as_ptr()) }
336323
}
337324

338-
reset_headers();
339-
340325
extern fn codespan(
341326
ob: *mut hoedown_buffer,
342327
text: *const hoedown_buffer,
@@ -500,18 +485,6 @@ impl LangString {
500485
}
501486
}
502487

503-
/// By default this markdown renderer generates anchors for each header in the
504-
/// rendered document. The anchor name is the contents of the header separated
505-
/// by hyphens, and a thread-local map is used to disambiguate among duplicate
506-
/// headers (numbers are appended).
507-
///
508-
/// This method will reset the local table for these headers. This is typically
509-
/// used at the beginning of rendering an entire HTML page to reset from the
510-
/// previous state (if any).
511-
pub fn reset_headers() {
512-
USED_HEADER_MAP.with(|s| s.borrow_mut().clear());
513-
}
514-
515488
impl<'a> fmt::Display for Markdown<'a> {
516489
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
517490
let Markdown(md) = *self;
@@ -579,6 +552,7 @@ pub fn plain_summary_line(md: &str) -> String {
579552
mod tests {
580553
use super::{LangString, Markdown};
581554
use super::plain_summary_line;
555+
use html::render::reset_ids;
582556

583557
#[test]
584558
fn test_lang_string_parse() {
@@ -611,13 +585,15 @@ mod tests {
611585
fn issue_17736() {
612586
let markdown = "# title";
613587
format!("{}", Markdown(markdown));
588+
reset_ids();
614589
}
615590

616591
#[test]
617592
fn test_header() {
618593
fn t(input: &str, expect: &str) {
619594
let output = format!("{}", Markdown(input));
620595
assert_eq!(output, expect);
596+
reset_ids();
621597
}
622598

623599
t("# Foo bar", "\n<h1 id='foo-bar' class='section-header'>\
@@ -634,6 +610,32 @@ mod tests {
634610
<em><code>baz</code></em> ❤ #qux</a></h4>");
635611
}
636612

613+
#[test]
614+
fn test_header_ids_multiple_blocks() {
615+
fn t(input: &str, expect: &str) {
616+
let output = format!("{}", Markdown(input));
617+
assert_eq!(output, expect);
618+
}
619+
620+
let test = || {
621+
t("# Example", "\n<h1 id='example' class='section-header'>\
622+
<a href='#example'>Example</a></h1>");
623+
t("# Panics", "\n<h1 id='panics' class='section-header'>\
624+
<a href='#panics'>Panics</a></h1>");
625+
t("# Example", "\n<h1 id='example-1' class='section-header'>\
626+
<a href='#example-1'>Example</a></h1>");
627+
t("# Main", "\n<h1 id='main-1' class='section-header'>\
628+
<a href='#main-1'>Main</a></h1>");
629+
t("# Example", "\n<h1 id='example-2' class='section-header'>\
630+
<a href='#example-2'>Example</a></h1>");
631+
t("# Panics", "\n<h1 id='panics-1' class='section-header'>\
632+
<a href='#panics-1'>Panics</a></h1>");
633+
};
634+
test();
635+
reset_ids();
636+
test();
637+
}
638+
637639
#[test]
638640
fn test_plain_summary_line() {
639641
fn t(input: &str, expect: &str) {

src/librustdoc/html/render.rs

+84-26
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,51 @@ impl fmt::Display for IndexItemFunctionType {
342342
thread_local!(static CACHE_KEY: RefCell<Arc<Cache>> = Default::default());
343343
thread_local!(pub static CURRENT_LOCATION_KEY: RefCell<Vec<String>> =
344344
RefCell::new(Vec::new()));
345+
thread_local!(static USED_ID_MAP: RefCell<HashMap<String, usize>> =
346+
RefCell::new(init_ids()));
347+
348+
fn init_ids() -> HashMap<String, usize> {
349+
[
350+
"main",
351+
"search",
352+
"help",
353+
"TOC",
354+
"render-detail",
355+
"associated-types",
356+
"associated-const",
357+
"required-methods",
358+
"provided-methods",
359+
"implementors",
360+
"implementors-list",
361+
"methods",
362+
"deref-methods",
363+
"implementations",
364+
"derived_implementations"
365+
].into_iter().map(|id| (String::from(*id), 1)).collect::<HashMap<_, _>>()
366+
}
367+
368+
/// This method resets the local table of used ID attributes. This is typically
369+
/// used at the beginning of rendering an entire HTML page to reset from the
370+
/// previous state (if any).
371+
pub fn reset_ids() {
372+
USED_ID_MAP.with(|s| *s.borrow_mut() = init_ids());
373+
}
374+
375+
pub fn derive_id(candidate: String) -> String {
376+
USED_ID_MAP.with(|map| {
377+
let id = match map.borrow_mut().get_mut(&candidate) {
378+
None => candidate,
379+
Some(a) => {
380+
let id = format!("{}-{}", candidate, *a);
381+
*a += 1;
382+
id
383+
}
384+
};
385+
386+
map.borrow_mut().insert(id.clone(), 1);
387+
id
388+
})
389+
}
345390

346391
/// Generates the documentation for `crate` into the directory `dst`
347392
pub fn run(mut krate: clean::Crate,
@@ -1276,7 +1321,7 @@ impl Context {
12761321
keywords: &keywords,
12771322
};
12781323

1279-
markdown::reset_headers();
1324+
reset_ids();
12801325

12811326
// We have a huge number of calls to write, so try to alleviate some
12821327
// of the pain by using a buffered writer instead of invoking the
@@ -1698,10 +1743,9 @@ fn item_module(w: &mut fmt::Formatter, cx: &Context,
16981743
ItemType::AssociatedType => ("associated-types", "Associated Types"),
16991744
ItemType::AssociatedConst => ("associated-consts", "Associated Constants"),
17001745
};
1701-
try!(write!(w,
1702-
"<h2 id='{id}' class='section-header'>\
1703-
<a href=\"#{id}\">{name}</a></h2>\n<table>",
1704-
id = short, name = name));
1746+
try!(write!(w, "<h2 id='{id}' class='section-header'>\
1747+
<a href=\"#{id}\">{name}</a></h2>\n<table>",
1748+
id = derive_id(short.to_owned()), name = name));
17051749
}
17061750

17071751
match myitem.inner {
@@ -1922,10 +1966,11 @@ fn item_trait(w: &mut fmt::Formatter, cx: &Context, it: &clean::Item,
19221966

19231967
fn trait_item(w: &mut fmt::Formatter, cx: &Context, m: &clean::Item)
19241968
-> fmt::Result {
1925-
try!(write!(w, "<h3 id='{ty}.{name}' class='method stab {stab}'><code>",
1926-
ty = shortty(m),
1927-
name = *m.name.as_ref().unwrap(),
1928-
stab = m.stability_class()));
1969+
let name = m.name.as_ref().unwrap();
1970+
let id = derive_id(format!("{}.{}", shortty(m), name));
1971+
try!(write!(w, "<h3 id='{id}' class='method stab {stab}'><code>",
1972+
id = id,
1973+
stab = m.stability_class()));
19291974
try!(render_assoc_item(w, m, AssocItemLink::Anchor));
19301975
try!(write!(w, "</code></h3>"));
19311976
try!(document(w, cx, m));
@@ -2420,44 +2465,38 @@ fn render_impl(w: &mut fmt::Formatter, cx: &Context, i: &Impl, link: AssocItemLi
24202465

24212466
fn doctraititem(w: &mut fmt::Formatter, cx: &Context, item: &clean::Item,
24222467
link: AssocItemLink, render_static: bool) -> fmt::Result {
2468+
let name = item.name.as_ref().unwrap();
24232469
match item.inner {
24242470
clean::MethodItem(..) | clean::TyMethodItem(..) => {
24252471
// Only render when the method is not static or we allow static methods
24262472
if !is_static_method(item) || render_static {
2427-
try!(write!(w, "<h4 id='method.{}' class='{}'><code>",
2428-
*item.name.as_ref().unwrap(),
2429-
shortty(item)));
2473+
let id = derive_id(format!("method.{}", name));
2474+
try!(write!(w, "<h4 id='{}' class='{}'><code>", id, shortty(item)));
24302475
try!(render_assoc_item(w, item, link));
24312476
try!(write!(w, "</code></h4>\n"));
24322477
}
24332478
}
24342479
clean::TypedefItem(ref tydef, _) => {
2435-
let name = item.name.as_ref().unwrap();
2436-
try!(write!(w, "<h4 id='assoc_type.{}' class='{}'><code>",
2437-
*name,
2438-
shortty(item)));
2480+
let id = derive_id(format!("assoc_type.{}", name));
2481+
try!(write!(w, "<h4 id='{}' class='{}'><code>", id, shortty(item)));
24392482
try!(write!(w, "type {} = {}", name, tydef.type_));
24402483
try!(write!(w, "</code></h4>\n"));
24412484
}
24422485
clean::AssociatedConstItem(ref ty, ref default) => {
2443-
let name = item.name.as_ref().unwrap();
2444-
try!(write!(w, "<h4 id='assoc_const.{}' class='{}'><code>",
2445-
*name, shortty(item)));
2486+
let id = derive_id(format!("assoc_const.{}", name));
2487+
try!(write!(w, "<h4 id='{}' class='{}'><code>", id, shortty(item)));
24462488
try!(assoc_const(w, item, ty, default.as_ref()));
24472489
try!(write!(w, "</code></h4>\n"));
24482490
}
24492491
clean::ConstantItem(ref c) => {
2450-
let name = item.name.as_ref().unwrap();
2451-
try!(write!(w, "<h4 id='assoc_const.{}' class='{}'><code>",
2452-
*name, shortty(item)));
2492+
let id = derive_id(format!("assoc_const.{}", name));
2493+
try!(write!(w, "<h4 id='{}' class='{}'><code>", id, shortty(item)));
24532494
try!(assoc_const(w, item, &c.type_, Some(&c.expr)));
24542495
try!(write!(w, "</code></h4>\n"));
24552496
}
24562497
clean::AssociatedTypeItem(ref bounds, ref default) => {
2457-
let name = item.name.as_ref().unwrap();
2458-
try!(write!(w, "<h4 id='assoc_type.{}' class='{}'><code>",
2459-
*name,
2460-
shortty(item)));
2498+
let id = derive_id(format!("assoc_type.{}", name));
2499+
try!(write!(w, "<h4 id='{}' class='{}'><code>", id, shortty(item)));
24612500
try!(assoc_type(w, item, bounds, default));
24622501
try!(write!(w, "</code></h4>\n"));
24632502
}
@@ -2671,3 +2710,22 @@ fn get_index_type_name(clean_type: &clean::Type) -> Option<String> {
26712710
pub fn cache() -> Arc<Cache> {
26722711
CACHE_KEY.with(|c| c.borrow().clone())
26732712
}
2713+
2714+
#[cfg(test)]
2715+
#[test]
2716+
fn test_unique_id() {
2717+
let input = ["foo", "examples", "examples", "method.into_iter","examples",
2718+
"method.into_iter", "foo", "main", "search", "methods",
2719+
"examples", "method.into_iter", "assoc_type.Item", "assoc_type.Item"];
2720+
let expected = ["foo", "examples", "examples-1", "method.into_iter", "examples-2",
2721+
"method.into_iter-1", "foo-1", "main-1", "search-1", "methods-1",
2722+
"examples-3", "method.into_iter-2", "assoc_type.Item", "assoc_type.Item-1"];
2723+
2724+
let test = || {
2725+
let actual: Vec<String> = input.iter().map(|s| derive_id(s.to_string())).collect();
2726+
assert_eq!(&actual[..], expected);
2727+
};
2728+
test();
2729+
reset_ids();
2730+
test();
2731+
}

src/librustdoc/markdown.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ use rustc::session::search_paths::SearchPaths;
2121

2222
use externalfiles::ExternalHtml;
2323

24+
use html::render::reset_ids;
2425
use html::escape::Escape;
2526
use html::markdown;
26-
use html::markdown::{Markdown, MarkdownWithToc, find_testable_code, reset_headers};
27+
use html::markdown::{Markdown, MarkdownWithToc, find_testable_code};
2728
use test::{TestOptions, Collector};
2829

2930
/// Separate any lines at the start of the file that begin with `%`.
@@ -82,7 +83,7 @@ pub fn render(input: &str, mut output: PathBuf, matches: &getopts::Matches,
8283
}
8384
let title = metadata[0];
8485

85-
reset_headers();
86+
reset_ids();
8687

8788
let rendered = if include_toc {
8889
format!("{}", MarkdownWithToc(text))

src/test/rustdoc/issue-25001.rs

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2015 The Rust Project Developers. See the COPYRIGHT
2+
// file at the top-level directory of this distribution and at
3+
// http://rust-lang.org/COPYRIGHT.
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
// @has issue_25001/struct.Foo.html
12+
pub struct Foo<T>(T);
13+
14+
pub trait Bar {
15+
type Item;
16+
17+
fn quux(self);
18+
}
19+
20+
impl<T> Foo<T> {
21+
// @has - '//*[@id="method.pass"]//code' 'fn pass()'
22+
pub fn pass() {}
23+
}
24+
impl<T> Foo<T> {
25+
// @has - '//*[@id="method.pass-1"]//code' 'fn pass() -> usize'
26+
pub fn pass() -> usize { 42 }
27+
}
28+
impl<T> Foo<T> {
29+
// @has - '//*[@id="method.pass-2"]//code' 'fn pass() -> isize'
30+
pub fn pass() -> isize { 42 }
31+
}
32+
33+
impl<T> Bar for Foo<T> {
34+
// @has - '//*[@id="assoc_type.Item"]//code' 'type Item = T'
35+
type Item=T;
36+
37+
// @has - '//*[@id="method.quux"]//code' 'fn quux(self)'
38+
fn quux(self) {}
39+
}
40+
impl<'a, T> Bar for &'a Foo<T> {
41+
// @has - '//*[@id="assoc_type.Item-1"]//code' "type Item = &'a T"
42+
type Item=&'a T;
43+
44+
// @has - '//*[@id="method.quux-1"]//code' 'fn quux(self)'
45+
fn quux(self) {}
46+
}
47+
impl<'a, T> Bar for &'a mut Foo<T> {
48+
// @has - '//*[@id="assoc_type.Item-2"]//code' "type Item = &'a mut T"
49+
type Item=&'a mut T;
50+
51+
// @has - '//*[@id="method.quux-2"]//code' 'fn quux(self)'
52+
fn quux(self) {}
53+
}

0 commit comments

Comments
 (0)