diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index f68e82501e91e..a5436886a7e8c 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -14,7 +14,7 @@ //! (bundled into the rust runtime). This module self-contains the C bindings //! and necessary legwork to render markdown, and exposes all of the //! functionality through a unit-struct, `Markdown`, which has an implementation -//! of `fmt::String`. Example usage: +//! of `fmt::Display`. Example usage: //! //! ```rust,ignore //! use rustdoc::html::markdown::Markdown; @@ -29,19 +29,19 @@ use libc; use std::ascii::AsciiExt; use std::cell::RefCell; -use std::collections::HashMap; use std::default::Default; use std::ffi::CString; use std::fmt; use std::slice; use std::str; +use html::render::derive_id; use html::toc::TocBuilder; use html::highlight; use html::escape::Escape; use test; -/// A unit struct which has the `fmt::String` trait implemented. When +/// A unit struct which has the `fmt::Display` trait implemented. When /// formatted, this struct will emit the HTML corresponding to the rendered /// version of the contained markdown string. pub struct Markdown<'a>(pub &'a str); @@ -210,10 +210,6 @@ fn collapse_whitespace(s: &str) -> String { s.split_whitespace().collect::>().join(" ") } -thread_local!(static USED_HEADER_MAP: RefCell> = { - RefCell::new(HashMap::new()) -}); - thread_local!(pub static PLAYGROUND_KRATE: RefCell>> = { RefCell::new(None) }); @@ -311,16 +307,7 @@ pub fn render(w: &mut fmt::Formatter, s: &str, print_toc: bool) -> fmt::Result { let opaque = unsafe { (*data).opaque as *mut hoedown_html_renderer_state }; let opaque = unsafe { &mut *((*opaque).opaque as *mut MyOpaque) }; - // Make sure our hyphenated ID is unique for this page - let id = USED_HEADER_MAP.with(|map| { - let id = match map.borrow_mut().get_mut(&id) { - None => id, - Some(a) => { *a += 1; format!("{}-{}", id, *a - 1) } - }; - map.borrow_mut().insert(id.clone(), 1); - id - }); - + let id = derive_id(id); let sec = opaque.toc_builder.as_mut().map_or("".to_owned(), |builder| { 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 { unsafe { hoedown_buffer_puts(ob, text.as_ptr()) } } - reset_headers(); - extern fn codespan( ob: *mut hoedown_buffer, text: *const hoedown_buffer, @@ -500,18 +485,6 @@ impl LangString { } } -/// By default this markdown renderer generates anchors for each header in the -/// rendered document. The anchor name is the contents of the header separated -/// by hyphens, and a thread-local map is used to disambiguate among duplicate -/// headers (numbers are appended). -/// -/// This method will reset the local table for these headers. This is typically -/// used at the beginning of rendering an entire HTML page to reset from the -/// previous state (if any). -pub fn reset_headers() { - USED_HEADER_MAP.with(|s| s.borrow_mut().clear()); -} - impl<'a> fmt::Display for Markdown<'a> { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { let Markdown(md) = *self; @@ -579,6 +552,7 @@ pub fn plain_summary_line(md: &str) -> String { mod tests { use super::{LangString, Markdown}; use super::plain_summary_line; + use html::render::reset_ids; #[test] fn test_lang_string_parse() { @@ -611,6 +585,7 @@ mod tests { fn issue_17736() { let markdown = "# title"; format!("{}", Markdown(markdown)); + reset_ids(); } #[test] @@ -618,6 +593,7 @@ mod tests { fn t(input: &str, expect: &str) { let output = format!("{}", Markdown(input)); assert_eq!(output, expect); + reset_ids(); } t("# Foo bar", "\n

\ @@ -634,6 +610,32 @@ mod tests { baz ❤ #qux

"); } + #[test] + fn test_header_ids_multiple_blocks() { + fn t(input: &str, expect: &str) { + let output = format!("{}", Markdown(input)); + assert_eq!(output, expect); + } + + let test = || { + t("# Example", "\n

\ + Example

"); + t("# Panics", "\n

\ + Panics

"); + t("# Example", "\n

\ + Example

"); + t("# Main", "\n

\ + Main

"); + t("# Example", "\n

\ + Example

"); + t("# Panics", "\n

\ + Panics

"); + }; + test(); + reset_ids(); + test(); + } + #[test] fn test_plain_summary_line() { fn t(input: &str, expect: &str) { diff --git a/src/librustdoc/html/render.rs b/src/librustdoc/html/render.rs index 574b9b599f5f8..58d7a36aac8a6 100644 --- a/src/librustdoc/html/render.rs +++ b/src/librustdoc/html/render.rs @@ -342,6 +342,51 @@ impl fmt::Display for IndexItemFunctionType { thread_local!(static CACHE_KEY: RefCell> = Default::default()); thread_local!(pub static CURRENT_LOCATION_KEY: RefCell> = RefCell::new(Vec::new())); +thread_local!(static USED_ID_MAP: RefCell> = + RefCell::new(init_ids())); + +fn init_ids() -> HashMap { + [ + "main", + "search", + "help", + "TOC", + "render-detail", + "associated-types", + "associated-const", + "required-methods", + "provided-methods", + "implementors", + "implementors-list", + "methods", + "deref-methods", + "implementations", + "derived_implementations" + ].into_iter().map(|id| (String::from(*id), 1)).collect::>() +} + +/// This method resets the local table of used ID attributes. This is typically +/// used at the beginning of rendering an entire HTML page to reset from the +/// previous state (if any). +pub fn reset_ids() { + USED_ID_MAP.with(|s| *s.borrow_mut() = init_ids()); +} + +pub fn derive_id(candidate: String) -> String { + USED_ID_MAP.with(|map| { + let id = match map.borrow_mut().get_mut(&candidate) { + None => candidate, + Some(a) => { + let id = format!("{}-{}", candidate, *a); + *a += 1; + id + } + }; + + map.borrow_mut().insert(id.clone(), 1); + id + }) +} /// Generates the documentation for `crate` into the directory `dst` pub fn run(mut krate: clean::Crate, @@ -1274,7 +1319,7 @@ impl Context { keywords: &keywords, }; - markdown::reset_headers(); + reset_ids(); // We have a huge number of calls to write, so try to alleviate some // of the pain by using a buffered writer instead of invoking the @@ -1696,10 +1741,9 @@ fn item_module(w: &mut fmt::Formatter, cx: &Context, ItemType::AssociatedType => ("associated-types", "Associated Types"), ItemType::AssociatedConst => ("associated-consts", "Associated Constants"), }; - try!(write!(w, - "

\ - {name}

\n", - id = short, name = name)); + try!(write!(w, "

\ + {name}

\n
", + id = derive_id(short.to_owned()), name = name)); } match myitem.inner { @@ -1920,10 +1964,11 @@ fn item_trait(w: &mut fmt::Formatter, cx: &Context, it: &clean::Item, fn trait_item(w: &mut fmt::Formatter, cx: &Context, m: &clean::Item) -> fmt::Result { - try!(write!(w, "

", - ty = shortty(m), - name = *m.name.as_ref().unwrap(), - stab = m.stability_class())); + let name = m.name.as_ref().unwrap(); + let id = derive_id(format!("{}.{}", shortty(m), name)); + try!(write!(w, "

", + id = id, + stab = m.stability_class())); try!(render_assoc_item(w, m, AssocItemLink::Anchor)); try!(write!(w, "

")); try!(document(w, cx, m)); @@ -2418,44 +2463,38 @@ fn render_impl(w: &mut fmt::Formatter, cx: &Context, i: &Impl, link: AssocItemLi fn doctraititem(w: &mut fmt::Formatter, cx: &Context, item: &clean::Item, link: AssocItemLink, render_static: bool) -> fmt::Result { + let name = item.name.as_ref().unwrap(); match item.inner { clean::MethodItem(..) | clean::TyMethodItem(..) => { // Only render when the method is not static or we allow static methods if !is_static_method(item) || render_static { - try!(write!(w, "

", - *item.name.as_ref().unwrap(), - shortty(item))); + let id = derive_id(format!("method.{}", name)); + try!(write!(w, "

", id, shortty(item))); try!(render_assoc_item(w, item, link)); try!(write!(w, "

\n")); } } clean::TypedefItem(ref tydef, _) => { - let name = item.name.as_ref().unwrap(); - try!(write!(w, "

", - *name, - shortty(item))); + let id = derive_id(format!("assoc_type.{}", name)); + try!(write!(w, "

", id, shortty(item))); try!(write!(w, "type {} = {}", name, tydef.type_)); try!(write!(w, "

\n")); } clean::AssociatedConstItem(ref ty, ref default) => { - let name = item.name.as_ref().unwrap(); - try!(write!(w, "

", - *name, shortty(item))); + let id = derive_id(format!("assoc_const.{}", name)); + try!(write!(w, "

", id, shortty(item))); try!(assoc_const(w, item, ty, default.as_ref())); try!(write!(w, "

\n")); } clean::ConstantItem(ref c) => { - let name = item.name.as_ref().unwrap(); - try!(write!(w, "

", - *name, shortty(item))); + let id = derive_id(format!("assoc_const.{}", name)); + try!(write!(w, "

", id, shortty(item))); try!(assoc_const(w, item, &c.type_, Some(&c.expr))); try!(write!(w, "

\n")); } clean::AssociatedTypeItem(ref bounds, ref default) => { - let name = item.name.as_ref().unwrap(); - try!(write!(w, "

", - *name, - shortty(item))); + let id = derive_id(format!("assoc_type.{}", name)); + try!(write!(w, "

", id, shortty(item))); try!(assoc_type(w, item, bounds, default)); try!(write!(w, "

\n")); } @@ -2669,3 +2708,22 @@ fn get_index_type_name(clean_type: &clean::Type) -> Option { pub fn cache() -> Arc { CACHE_KEY.with(|c| c.borrow().clone()) } + +#[cfg(test)] +#[test] +fn test_unique_id() { + let input = ["foo", "examples", "examples", "method.into_iter","examples", + "method.into_iter", "foo", "main", "search", "methods", + "examples", "method.into_iter", "assoc_type.Item", "assoc_type.Item"]; + let expected = ["foo", "examples", "examples-1", "method.into_iter", "examples-2", + "method.into_iter-1", "foo-1", "main-1", "search-1", "methods-1", + "examples-3", "method.into_iter-2", "assoc_type.Item", "assoc_type.Item-1"]; + + let test = || { + let actual: Vec = input.iter().map(|s| derive_id(s.to_string())).collect(); + assert_eq!(&actual[..], expected); + }; + test(); + reset_ids(); + test(); +} diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs index a311b938e9609..ac64fd3bec0e7 100644 --- a/src/librustdoc/markdown.rs +++ b/src/librustdoc/markdown.rs @@ -21,9 +21,10 @@ use rustc::session::search_paths::SearchPaths; use externalfiles::ExternalHtml; +use html::render::reset_ids; use html::escape::Escape; use html::markdown; -use html::markdown::{Markdown, MarkdownWithToc, find_testable_code, reset_headers}; +use html::markdown::{Markdown, MarkdownWithToc, find_testable_code}; use test::{TestOptions, Collector}; /// 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, } let title = metadata[0]; - reset_headers(); + reset_ids(); let rendered = if include_toc { format!("{}", MarkdownWithToc(text)) diff --git a/src/test/rustdoc/issue-25001.rs b/src/test/rustdoc/issue-25001.rs new file mode 100644 index 0000000000000..e4d97828d50a0 --- /dev/null +++ b/src/test/rustdoc/issue-25001.rs @@ -0,0 +1,53 @@ +// Copyright 2015 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// @has issue_25001/struct.Foo.html +pub struct Foo(T); + +pub trait Bar { + type Item; + + fn quux(self); +} + +impl Foo { + // @has - '//*[@id="method.pass"]//code' 'fn pass()' + pub fn pass() {} +} +impl Foo { + // @has - '//*[@id="method.pass-1"]//code' 'fn pass() -> usize' + pub fn pass() -> usize { 42 } +} +impl Foo { + // @has - '//*[@id="method.pass-2"]//code' 'fn pass() -> isize' + pub fn pass() -> isize { 42 } +} + +impl Bar for Foo { + // @has - '//*[@id="assoc_type.Item"]//code' 'type Item = T' + type Item=T; + + // @has - '//*[@id="method.quux"]//code' 'fn quux(self)' + fn quux(self) {} +} +impl<'a, T> Bar for &'a Foo { + // @has - '//*[@id="assoc_type.Item-1"]//code' "type Item = &'a T" + type Item=&'a T; + + // @has - '//*[@id="method.quux-1"]//code' 'fn quux(self)' + fn quux(self) {} +} +impl<'a, T> Bar for &'a mut Foo { + // @has - '//*[@id="assoc_type.Item-2"]//code' "type Item = &'a mut T" + type Item=&'a mut T; + + // @has - '//*[@id="method.quux-2"]//code' 'fn quux(self)' + fn quux(self) {} +} diff --git a/src/test/rustdoc/issue-29449.rs b/src/test/rustdoc/issue-29449.rs new file mode 100644 index 0000000000000..f296048e30b54 --- /dev/null +++ b/src/test/rustdoc/issue-29449.rs @@ -0,0 +1,30 @@ +// Copyright 2015 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +// @has issue_29449/struct.Foo.html +pub struct Foo; + +impl Foo { + // @has - '//*[@id="examples"]//a' 'Examples' + // @has - '//*[@id="panics"]//a' 'Panics' + /// # Examples + /// # Panics + pub fn bar() {} + + // @has - '//*[@id="examples-1"]//a' 'Examples' + /// # Examples + pub fn bar_1() {} + + // @has - '//*[@id="examples-2"]//a' 'Examples' + // @has - '//*[@id="panics-1"]//a' 'Panics' + /// # Examples + /// # Panics + pub fn bar_2() {} +}