Skip to content

Commit f22c96c

Browse files
committed
rustdoc: add table-of-contents recording & rendering, use it with plain
markdown files. This means that # Foo ## Bar # Baz ### Qux ## Quz Gets a TOC like 1 Foo 1.1 Bar 2 Baz 2.0.1 Qux 2.1 Quz This functionality is only used when rendering a single markdown file, never on an individual module, although it could very feasibly be extended to allow modules to opt-in to a table of contents (std::fmt comes to mind).
1 parent 69b8ef8 commit f22c96c

File tree

4 files changed

+312
-12
lines changed

4 files changed

+312
-12
lines changed

src/librustdoc/html/markdown.rs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,16 @@ use std::str;
3636
use std::vec;
3737
use collections::HashMap;
3838

39+
use html::toc::TocBuilder;
3940
use html::highlight;
4041

4142
/// A unit struct which has the `fmt::Show` trait implemented. When
4243
/// formatted, this struct will emit the HTML corresponding to the rendered
4344
/// version of the contained markdown string.
4445
pub struct Markdown<'a>(&'a str);
46+
/// A unit struct like `Markdown`, that renders the markdown with a
47+
/// table of contents.
48+
pub struct MarkdownWithToc<'a>(&'a str);
4549

4650
static OUTPUT_UNIT: libc::size_t = 64;
4751
static MKDEXT_NO_INTRA_EMPHASIS: libc::c_uint = 1 << 0;
@@ -75,6 +79,7 @@ struct html_renderopt {
7579
struct my_opaque {
7680
opt: html_renderopt,
7781
dfltblk: extern "C" fn(*buf, *buf, *buf, *libc::c_void),
82+
toc_builder: Option<TocBuilder>,
7883
}
7984

8085
struct buf {
@@ -121,7 +126,7 @@ fn stripped_filtered_line<'a>(s: &'a str) -> Option<&'a str> {
121126

122127
local_data_key!(used_header_map: HashMap<~str, uint>)
123128

124-
pub fn render(w: &mut io::Writer, s: &str) -> fmt::Result {
129+
pub fn render(w: &mut io::Writer, s: &str, print_toc: bool) -> fmt::Result {
125130
extern fn block(ob: *buf, text: *buf, lang: *buf, opaque: *libc::c_void) {
126131
unsafe {
127132
let my_opaque: &my_opaque = cast::transmute(opaque);
@@ -162,7 +167,7 @@ pub fn render(w: &mut io::Writer, s: &str) -> fmt::Result {
162167
}
163168

164169
extern fn header(ob: *buf, text: *buf, level: libc::c_int,
165-
_opaque: *libc::c_void) {
170+
opaque: *libc::c_void) {
166171
// sundown does this, we may as well too
167172
"\n".with_c_str(|p| unsafe { bufputs(ob, p) });
168173

@@ -183,6 +188,8 @@ pub fn render(w: &mut io::Writer, s: &str) -> fmt::Result {
183188
}
184189
}).to_owned_vec().connect("-");
185190

191+
let opaque = unsafe {&mut *(opaque as *mut my_opaque)};
192+
186193
// Make sure our hyphenated ID is unique for this page
187194
let id = local_data::get_mut(used_header_map, |map| {
188195
let map = map.unwrap();
@@ -194,9 +201,18 @@ pub fn render(w: &mut io::Writer, s: &str) -> fmt::Result {
194201
id.clone()
195202
});
196203

204+
let sec = match opaque.toc_builder {
205+
Some(ref mut builder) => {
206+
builder.push(level as u32, s.clone(), id.clone())
207+
}
208+
None => {""}
209+
};
210+
197211
// Render the HTML
198-
let text = format!(r#"<h{lvl} id="{id}">{}</h{lvl}>"#,
199-
s, lvl = level, id = id);
212+
let text = format!(r#"<h{lvl} id="{id}">{sec_len,plural,=0{}other{{sec} }}{}</h{lvl}>"#,
213+
s, lvl = level, id = id,
214+
sec_len = sec.len(), sec = sec);
215+
200216
text.with_c_str(|p| unsafe { bufputs(ob, p) });
201217
}
202218

@@ -218,23 +234,30 @@ pub fn render(w: &mut io::Writer, s: &str) -> fmt::Result {
218234
let mut callbacks: sd_callbacks = mem::init();
219235

220236
sdhtml_renderer(&callbacks, &options, 0);
221-
let opaque = my_opaque {
237+
let mut opaque = my_opaque {
222238
opt: options,
223239
dfltblk: callbacks.blockcode.unwrap(),
240+
toc_builder: if print_toc {Some(TocBuilder::new())} else {None}
224241
};
225242
callbacks.blockcode = Some(block);
226243
callbacks.header = Some(header);
227244
let markdown = sd_markdown_new(extensions, 16, &callbacks,
228-
&opaque as *my_opaque as *libc::c_void);
245+
&mut opaque as *mut my_opaque as *libc::c_void);
229246

230247

231248
sd_markdown_render(ob, s.as_ptr(), s.len() as libc::size_t, markdown);
232249
sd_markdown_free(markdown);
233250

234-
let ret = vec::raw::buf_as_slice((*ob).data, (*ob).size as uint, |buf| {
235-
w.write(buf)
236-
});
251+
let mut ret = match opaque.toc_builder {
252+
Some(b) => write!(w, "<nav id=\"TOC\">{}</nav>", b.into_toc()),
253+
None => Ok(())
254+
};
237255

256+
if ret.is_ok() {
257+
ret = vec::raw::buf_as_slice((*ob).data, (*ob).size as uint, |buf| {
258+
w.write(buf)
259+
});
260+
}
238261
bufrelease(ob);
239262
ret
240263
}
@@ -319,6 +342,13 @@ impl<'a> fmt::Show for Markdown<'a> {
319342
let Markdown(md) = *self;
320343
// This is actually common enough to special-case
321344
if md.len() == 0 { return Ok(()) }
322-
render(fmt.buf, md.as_slice())
345+
render(fmt.buf, md.as_slice(), false)
346+
}
347+
}
348+
349+
impl<'a> fmt::Show for MarkdownWithToc<'a> {
350+
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
351+
let MarkdownWithToc(md) = *self;
352+
render(fmt.buf, md.as_slice(), true)
323353
}
324354
}

src/librustdoc/html/toc.rs

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
// Copyright 2013 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+
//! Table-of-contents creation.
12+
13+
use std::fmt;
14+
use std::vec_ng::Vec;
15+
16+
/// A (recursive) table of contents
17+
#[deriving(Eq)]
18+
pub struct Toc {
19+
/// The levels are strictly decreasing, i.e.
20+
///
21+
/// entries[0].level >= entries[1].level >= ...
22+
///
23+
/// Normally they are equal, but can differ in cases like A and B,
24+
/// both of which end up in the same `Toc` as they have the same
25+
/// parent (Main).
26+
///
27+
/// # Main
28+
/// ### A
29+
/// ## B
30+
priv entries: Vec<TocEntry>
31+
}
32+
33+
impl Toc {
34+
fn count_entries_with_level(&self, level: u32) -> uint {
35+
self.entries.iter().count(|e| e.level == level)
36+
}
37+
}
38+
39+
#[deriving(Eq)]
40+
pub struct TocEntry {
41+
priv level: u32,
42+
priv sec_number: ~str,
43+
priv name: ~str,
44+
priv id: ~str,
45+
priv children: Toc,
46+
}
47+
48+
/// Progressive construction of a table of contents.
49+
#[deriving(Eq)]
50+
pub struct TocBuilder {
51+
priv top_level: Toc,
52+
/// The current heirachy of parent headings, the levels are
53+
/// strictly increasing (i.e. chain[0].level < chain[1].level <
54+
/// ...) with each entry being the most recent occurance of a
55+
/// heading with that level (it doesn't include the most recent
56+
/// occurences of every level, just, if *is* in `chain` then is is
57+
/// the most recent one).
58+
///
59+
/// We also have `chain[0].level <= top_level.entries[last]`.
60+
priv chain: Vec<TocEntry>
61+
}
62+
63+
impl TocBuilder {
64+
pub fn new() -> TocBuilder {
65+
TocBuilder { top_level: Toc { entries: Vec::new() }, chain: Vec::new() }
66+
}
67+
68+
69+
/// Convert into a true `Toc` struct.
70+
pub fn into_toc(mut self) -> Toc {
71+
// we know all levels are >= 1.
72+
self.fold_until(0);
73+
self.top_level
74+
}
75+
76+
/// Collapse the chain until the first heading more important than
77+
/// `level` (i.e. lower level)
78+
///
79+
/// Example:
80+
///
81+
/// ## A
82+
/// # B
83+
/// # C
84+
/// ## D
85+
/// ## E
86+
/// ### F
87+
/// #### G
88+
/// ### H
89+
///
90+
/// If we are considering H (i.e. level 3), then A and B are in
91+
/// self.top_level, D is in C.children, and C, E, F, G are in
92+
/// self.chain.
93+
///
94+
/// When we attempt to push H, we realise that first G is not the
95+
/// parent (level is too high) so it is popped from chain and put
96+
/// into F.children, then F isn't the parent (level is equal, aka
97+
/// sibling), so it's also popped and put into E.children.
98+
///
99+
/// This leaves us looking at E, which does have a smaller level,
100+
/// and, by construction, it's the most recent thing with smaller
101+
/// level, i.e. it's the immediate parent of H.
102+
fn fold_until(&mut self, level: u32) {
103+
let mut this = None;
104+
loop {
105+
match self.chain.pop() {
106+
Some(mut next) => {
107+
this.map(|e| next.children.entries.push(e));
108+
if next.level < level {
109+
// this is the parent we want, so return it to
110+
// its rightful place.
111+
self.chain.push(next);
112+
return
113+
} else {
114+
this = Some(next);
115+
}
116+
}
117+
None => {
118+
this.map(|e| self.top_level.entries.push(e));
119+
return
120+
}
121+
}
122+
}
123+
}
124+
125+
/// Push a level `level` heading into the appropriate place in the
126+
/// heirarchy, returning a string containing the section number in
127+
/// `<num>.<num>.<num>` format.
128+
pub fn push<'a>(&'a mut self, level: u32, name: ~str, id: ~str) -> &'a str {
129+
assert!(level >= 1);
130+
131+
// collapse all previous sections into their parents until we
132+
// get to relevant heading (i.e. the first one with a smaller
133+
// level than us)
134+
self.fold_until(level);
135+
136+
let mut sec_number;
137+
{
138+
let (toc_level, toc) = match self.chain.last() {
139+
None => {
140+
sec_number = ~"";
141+
(0, &self.top_level)
142+
}
143+
Some(entry) => {
144+
sec_number = entry.sec_number.clone();
145+
sec_number.push_str(".");
146+
(entry.level, &entry.children)
147+
}
148+
};
149+
// fill in any missing zeros, e.g. for
150+
// # Foo (1)
151+
// ### Bar (1.0.1)
152+
for _ in range(toc_level, level - 1) {
153+
sec_number.push_str("0.");
154+
}
155+
let number = toc.count_entries_with_level(level);
156+
sec_number.push_str(format!("{}", number + 1))
157+
}
158+
159+
self.chain.push(TocEntry {
160+
level: level,
161+
name: name,
162+
sec_number: sec_number,
163+
id: id,
164+
children: Toc { entries: Vec::new() }
165+
});
166+
167+
// get the thing we just pushed, so we can borrow the string
168+
// out of it with the right lifetime
169+
let just_inserted = self.chain.mut_last().unwrap();
170+
just_inserted.sec_number.as_slice()
171+
}
172+
}
173+
174+
impl fmt::Show for Toc {
175+
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
176+
try!(write!(fmt.buf, "<ul>"));
177+
for entry in self.entries.iter() {
178+
// recursively format this table of contents (the
179+
// `{children}` is the key).
180+
try!(write!(fmt.buf,
181+
"\n<li><a href=\"\\#{id}\">{num} {name}</a>{children}</li>",
182+
id = entry.id,
183+
num = entry.sec_number, name = entry.name,
184+
children = entry.children))
185+
}
186+
write!(fmt.buf, "</ul>")
187+
}
188+
}
189+
190+
#[cfg(test)]
191+
mod test {
192+
use super::{TocBuilder, Toc, TocEntry};
193+
194+
#[test]
195+
fn builder_smoke() {
196+
let mut builder = TocBuilder::new();
197+
198+
// this is purposely not using a fancy macro like below so
199+
// that we're sure that this is doing the correct thing, and
200+
// there's been no macro mistake.
201+
macro_rules! push {
202+
($level: expr, $name: expr) => {
203+
assert_eq!(builder.push($level, $name.to_owned(), ~""), $name);
204+
}
205+
}
206+
push!(2, "0.1");
207+
push!(1, "1");
208+
{
209+
push!(2, "1.1");
210+
{
211+
push!(3, "1.1.1");
212+
push!(3, "1.1.2");
213+
}
214+
push!(2, "1.2");
215+
{
216+
push!(3, "1.2.1");
217+
push!(3, "1.2.2");
218+
}
219+
}
220+
push!(1, "2");
221+
push!(1, "3");
222+
{
223+
push!(4, "3.0.0.1");
224+
{
225+
push!(6, "3.0.0.1.0.1");
226+
}
227+
push!(4, "3.0.0.2");
228+
push!(2, "3.1");
229+
{
230+
push!(4, "3.1.0.1");
231+
}
232+
}
233+
234+
macro_rules! toc {
235+
($(($level: expr, $name: expr, $(($sub: tt))* )),*) => {
236+
Toc {
237+
entries: vec!(
238+
$(
239+
TocEntry {
240+
level: $level,
241+
name: $name.to_owned(),
242+
sec_number: $name.to_owned(),
243+
id: ~"",
244+
children: toc!($($sub),*)
245+
}
246+
),*
247+
)
248+
}
249+
}
250+
}
251+
let expected = toc!(
252+
(2, "0.1", ),
253+
254+
(1, "1",
255+
((2, "1.1", ((3, "1.1.1", )) ((3, "1.1.2", ))))
256+
((2, "1.2", ((3, "1.2.1", )) ((3, "1.2.2", ))))
257+
),
258+
259+
(1, "2", ),
260+
261+
(1, "3",
262+
((4, "3.0.0.1", ((6, "3.0.0.1.0.1", ))))
263+
((4, "3.0.0.2", ))
264+
((2, "3.1", ((4, "3.1.0.1", ))))
265+
)
266+
);
267+
assert_eq!(expected, builder.into_toc());
268+
}
269+
}

0 commit comments

Comments
 (0)