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).
This commit is contained in:
parent
69b8ef806b
commit
f22c96cc88
|
@ -36,12 +36,16 @@ use std::str;
|
|||
use std::vec;
|
||||
use collections::HashMap;
|
||||
|
||||
use html::toc::TocBuilder;
|
||||
use html::highlight;
|
||||
|
||||
/// A unit struct which has the `fmt::Show` trait implemented. When
|
||||
/// formatted, this struct will emit the HTML corresponding to the rendered
|
||||
/// version of the contained markdown string.
|
||||
pub struct Markdown<'a>(&'a str);
|
||||
/// A unit struct like `Markdown`, that renders the markdown with a
|
||||
/// table of contents.
|
||||
pub struct MarkdownWithToc<'a>(&'a str);
|
||||
|
||||
static OUTPUT_UNIT: libc::size_t = 64;
|
||||
static MKDEXT_NO_INTRA_EMPHASIS: libc::c_uint = 1 << 0;
|
||||
|
@ -75,6 +79,7 @@ struct html_renderopt {
|
|||
struct my_opaque {
|
||||
opt: html_renderopt,
|
||||
dfltblk: extern "C" fn(*buf, *buf, *buf, *libc::c_void),
|
||||
toc_builder: Option<TocBuilder>,
|
||||
}
|
||||
|
||||
struct buf {
|
||||
|
@ -121,7 +126,7 @@ fn stripped_filtered_line<'a>(s: &'a str) -> Option<&'a str> {
|
|||
|
||||
local_data_key!(used_header_map: HashMap<~str, uint>)
|
||||
|
||||
pub fn render(w: &mut io::Writer, s: &str) -> fmt::Result {
|
||||
pub fn render(w: &mut io::Writer, s: &str, print_toc: bool) -> fmt::Result {
|
||||
extern fn block(ob: *buf, text: *buf, lang: *buf, opaque: *libc::c_void) {
|
||||
unsafe {
|
||||
let my_opaque: &my_opaque = cast::transmute(opaque);
|
||||
|
@ -162,7 +167,7 @@ pub fn render(w: &mut io::Writer, s: &str) -> fmt::Result {
|
|||
}
|
||||
|
||||
extern fn header(ob: *buf, text: *buf, level: libc::c_int,
|
||||
_opaque: *libc::c_void) {
|
||||
opaque: *libc::c_void) {
|
||||
// sundown does this, we may as well too
|
||||
"\n".with_c_str(|p| unsafe { bufputs(ob, p) });
|
||||
|
||||
|
@ -183,6 +188,8 @@ pub fn render(w: &mut io::Writer, s: &str) -> fmt::Result {
|
|||
}
|
||||
}).to_owned_vec().connect("-");
|
||||
|
||||
let opaque = unsafe {&mut *(opaque as *mut my_opaque)};
|
||||
|
||||
// Make sure our hyphenated ID is unique for this page
|
||||
let id = local_data::get_mut(used_header_map, |map| {
|
||||
let map = map.unwrap();
|
||||
|
@ -194,9 +201,18 @@ pub fn render(w: &mut io::Writer, s: &str) -> fmt::Result {
|
|||
id.clone()
|
||||
});
|
||||
|
||||
let sec = match opaque.toc_builder {
|
||||
Some(ref mut builder) => {
|
||||
builder.push(level as u32, s.clone(), id.clone())
|
||||
}
|
||||
None => {""}
|
||||
};
|
||||
|
||||
// Render the HTML
|
||||
let text = format!(r#"<h{lvl} id="{id}">{}</h{lvl}>"#,
|
||||
s, lvl = level, id = id);
|
||||
let text = format!(r#"<h{lvl} id="{id}">{sec_len,plural,=0{}other{{sec} }}{}</h{lvl}>"#,
|
||||
s, lvl = level, id = id,
|
||||
sec_len = sec.len(), sec = sec);
|
||||
|
||||
text.with_c_str(|p| unsafe { bufputs(ob, p) });
|
||||
}
|
||||
|
||||
|
@ -218,23 +234,30 @@ pub fn render(w: &mut io::Writer, s: &str) -> fmt::Result {
|
|||
let mut callbacks: sd_callbacks = mem::init();
|
||||
|
||||
sdhtml_renderer(&callbacks, &options, 0);
|
||||
let opaque = my_opaque {
|
||||
let mut opaque = my_opaque {
|
||||
opt: options,
|
||||
dfltblk: callbacks.blockcode.unwrap(),
|
||||
toc_builder: if print_toc {Some(TocBuilder::new())} else {None}
|
||||
};
|
||||
callbacks.blockcode = Some(block);
|
||||
callbacks.header = Some(header);
|
||||
let markdown = sd_markdown_new(extensions, 16, &callbacks,
|
||||
&opaque as *my_opaque as *libc::c_void);
|
||||
&mut opaque as *mut my_opaque as *libc::c_void);
|
||||
|
||||
|
||||
sd_markdown_render(ob, s.as_ptr(), s.len() as libc::size_t, markdown);
|
||||
sd_markdown_free(markdown);
|
||||
|
||||
let ret = vec::raw::buf_as_slice((*ob).data, (*ob).size as uint, |buf| {
|
||||
w.write(buf)
|
||||
});
|
||||
let mut ret = match opaque.toc_builder {
|
||||
Some(b) => write!(w, "<nav id=\"TOC\">{}</nav>", b.into_toc()),
|
||||
None => Ok(())
|
||||
};
|
||||
|
||||
if ret.is_ok() {
|
||||
ret = vec::raw::buf_as_slice((*ob).data, (*ob).size as uint, |buf| {
|
||||
w.write(buf)
|
||||
});
|
||||
}
|
||||
bufrelease(ob);
|
||||
ret
|
||||
}
|
||||
|
@ -319,6 +342,13 @@ impl<'a> fmt::Show for Markdown<'a> {
|
|||
let Markdown(md) = *self;
|
||||
// This is actually common enough to special-case
|
||||
if md.len() == 0 { return Ok(()) }
|
||||
render(fmt.buf, md.as_slice())
|
||||
render(fmt.buf, md.as_slice(), false)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Show for MarkdownWithToc<'a> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
let MarkdownWithToc(md) = *self;
|
||||
render(fmt.buf, md.as_slice(), true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
// Copyright 2013 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 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
//! Table-of-contents creation.
|
||||
|
||||
use std::fmt;
|
||||
use std::vec_ng::Vec;
|
||||
|
||||
/// A (recursive) table of contents
|
||||
#[deriving(Eq)]
|
||||
pub struct Toc {
|
||||
/// The levels are strictly decreasing, i.e.
|
||||
///
|
||||
/// entries[0].level >= entries[1].level >= ...
|
||||
///
|
||||
/// Normally they are equal, but can differ in cases like A and B,
|
||||
/// both of which end up in the same `Toc` as they have the same
|
||||
/// parent (Main).
|
||||
///
|
||||
/// # Main
|
||||
/// ### A
|
||||
/// ## B
|
||||
priv entries: Vec<TocEntry>
|
||||
}
|
||||
|
||||
impl Toc {
|
||||
fn count_entries_with_level(&self, level: u32) -> uint {
|
||||
self.entries.iter().count(|e| e.level == level)
|
||||
}
|
||||
}
|
||||
|
||||
#[deriving(Eq)]
|
||||
pub struct TocEntry {
|
||||
priv level: u32,
|
||||
priv sec_number: ~str,
|
||||
priv name: ~str,
|
||||
priv id: ~str,
|
||||
priv children: Toc,
|
||||
}
|
||||
|
||||
/// Progressive construction of a table of contents.
|
||||
#[deriving(Eq)]
|
||||
pub struct TocBuilder {
|
||||
priv top_level: Toc,
|
||||
/// The current heirachy of parent headings, the levels are
|
||||
/// strictly increasing (i.e. chain[0].level < chain[1].level <
|
||||
/// ...) with each entry being the most recent occurance of a
|
||||
/// heading with that level (it doesn't include the most recent
|
||||
/// occurences of every level, just, if *is* in `chain` then is is
|
||||
/// the most recent one).
|
||||
///
|
||||
/// We also have `chain[0].level <= top_level.entries[last]`.
|
||||
priv chain: Vec<TocEntry>
|
||||
}
|
||||
|
||||
impl TocBuilder {
|
||||
pub fn new() -> TocBuilder {
|
||||
TocBuilder { top_level: Toc { entries: Vec::new() }, chain: Vec::new() }
|
||||
}
|
||||
|
||||
|
||||
/// Convert into a true `Toc` struct.
|
||||
pub fn into_toc(mut self) -> Toc {
|
||||
// we know all levels are >= 1.
|
||||
self.fold_until(0);
|
||||
self.top_level
|
||||
}
|
||||
|
||||
/// Collapse the chain until the first heading more important than
|
||||
/// `level` (i.e. lower level)
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ## A
|
||||
/// # B
|
||||
/// # C
|
||||
/// ## D
|
||||
/// ## E
|
||||
/// ### F
|
||||
/// #### G
|
||||
/// ### H
|
||||
///
|
||||
/// If we are considering H (i.e. level 3), then A and B are in
|
||||
/// self.top_level, D is in C.children, and C, E, F, G are in
|
||||
/// self.chain.
|
||||
///
|
||||
/// When we attempt to push H, we realise that first G is not the
|
||||
/// parent (level is too high) so it is popped from chain and put
|
||||
/// into F.children, then F isn't the parent (level is equal, aka
|
||||
/// sibling), so it's also popped and put into E.children.
|
||||
///
|
||||
/// This leaves us looking at E, which does have a smaller level,
|
||||
/// and, by construction, it's the most recent thing with smaller
|
||||
/// level, i.e. it's the immediate parent of H.
|
||||
fn fold_until(&mut self, level: u32) {
|
||||
let mut this = None;
|
||||
loop {
|
||||
match self.chain.pop() {
|
||||
Some(mut next) => {
|
||||
this.map(|e| next.children.entries.push(e));
|
||||
if next.level < level {
|
||||
// this is the parent we want, so return it to
|
||||
// its rightful place.
|
||||
self.chain.push(next);
|
||||
return
|
||||
} else {
|
||||
this = Some(next);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
this.map(|e| self.top_level.entries.push(e));
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a level `level` heading into the appropriate place in the
|
||||
/// heirarchy, returning a string containing the section number in
|
||||
/// `<num>.<num>.<num>` format.
|
||||
pub fn push<'a>(&'a mut self, level: u32, name: ~str, id: ~str) -> &'a str {
|
||||
assert!(level >= 1);
|
||||
|
||||
// collapse all previous sections into their parents until we
|
||||
// get to relevant heading (i.e. the first one with a smaller
|
||||
// level than us)
|
||||
self.fold_until(level);
|
||||
|
||||
let mut sec_number;
|
||||
{
|
||||
let (toc_level, toc) = match self.chain.last() {
|
||||
None => {
|
||||
sec_number = ~"";
|
||||
(0, &self.top_level)
|
||||
}
|
||||
Some(entry) => {
|
||||
sec_number = entry.sec_number.clone();
|
||||
sec_number.push_str(".");
|
||||
(entry.level, &entry.children)
|
||||
}
|
||||
};
|
||||
// fill in any missing zeros, e.g. for
|
||||
// # Foo (1)
|
||||
// ### Bar (1.0.1)
|
||||
for _ in range(toc_level, level - 1) {
|
||||
sec_number.push_str("0.");
|
||||
}
|
||||
let number = toc.count_entries_with_level(level);
|
||||
sec_number.push_str(format!("{}", number + 1))
|
||||
}
|
||||
|
||||
self.chain.push(TocEntry {
|
||||
level: level,
|
||||
name: name,
|
||||
sec_number: sec_number,
|
||||
id: id,
|
||||
children: Toc { entries: Vec::new() }
|
||||
});
|
||||
|
||||
// get the thing we just pushed, so we can borrow the string
|
||||
// out of it with the right lifetime
|
||||
let just_inserted = self.chain.mut_last().unwrap();
|
||||
just_inserted.sec_number.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Show for Toc {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
try!(write!(fmt.buf, "<ul>"));
|
||||
for entry in self.entries.iter() {
|
||||
// recursively format this table of contents (the
|
||||
// `{children}` is the key).
|
||||
try!(write!(fmt.buf,
|
||||
"\n<li><a href=\"\\#{id}\">{num} {name}</a>{children}</li>",
|
||||
id = entry.id,
|
||||
num = entry.sec_number, name = entry.name,
|
||||
children = entry.children))
|
||||
}
|
||||
write!(fmt.buf, "</ul>")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{TocBuilder, Toc, TocEntry};
|
||||
|
||||
#[test]
|
||||
fn builder_smoke() {
|
||||
let mut builder = TocBuilder::new();
|
||||
|
||||
// this is purposely not using a fancy macro like below so
|
||||
// that we're sure that this is doing the correct thing, and
|
||||
// there's been no macro mistake.
|
||||
macro_rules! push {
|
||||
($level: expr, $name: expr) => {
|
||||
assert_eq!(builder.push($level, $name.to_owned(), ~""), $name);
|
||||
}
|
||||
}
|
||||
push!(2, "0.1");
|
||||
push!(1, "1");
|
||||
{
|
||||
push!(2, "1.1");
|
||||
{
|
||||
push!(3, "1.1.1");
|
||||
push!(3, "1.1.2");
|
||||
}
|
||||
push!(2, "1.2");
|
||||
{
|
||||
push!(3, "1.2.1");
|
||||
push!(3, "1.2.2");
|
||||
}
|
||||
}
|
||||
push!(1, "2");
|
||||
push!(1, "3");
|
||||
{
|
||||
push!(4, "3.0.0.1");
|
||||
{
|
||||
push!(6, "3.0.0.1.0.1");
|
||||
}
|
||||
push!(4, "3.0.0.2");
|
||||
push!(2, "3.1");
|
||||
{
|
||||
push!(4, "3.1.0.1");
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! toc {
|
||||
($(($level: expr, $name: expr, $(($sub: tt))* )),*) => {
|
||||
Toc {
|
||||
entries: vec!(
|
||||
$(
|
||||
TocEntry {
|
||||
level: $level,
|
||||
name: $name.to_owned(),
|
||||
sec_number: $name.to_owned(),
|
||||
id: ~"",
|
||||
children: toc!($($sub),*)
|
||||
}
|
||||
),*
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
let expected = toc!(
|
||||
(2, "0.1", ),
|
||||
|
||||
(1, "1",
|
||||
((2, "1.1", ((3, "1.1.1", )) ((3, "1.1.2", ))))
|
||||
((2, "1.2", ((3, "1.2.1", )) ((3, "1.2.2", ))))
|
||||
),
|
||||
|
||||
(1, "2", ),
|
||||
|
||||
(1, "3",
|
||||
((4, "3.0.0.1", ((6, "3.0.0.1.0.1", ))))
|
||||
((4, "3.0.0.2", ))
|
||||
((2, "3.1", ((4, "3.1.0.1", ))))
|
||||
)
|
||||
);
|
||||
assert_eq!(expected, builder.into_toc());
|
||||
}
|
||||
}
|
|
@ -44,6 +44,7 @@ pub mod html {
|
|||
pub mod layout;
|
||||
pub mod markdown;
|
||||
pub mod render;
|
||||
pub mod toc;
|
||||
}
|
||||
pub mod markdown;
|
||||
pub mod passes;
|
||||
|
|
|
@ -18,7 +18,7 @@ use getopts;
|
|||
use testing;
|
||||
|
||||
use html::escape::Escape;
|
||||
use html::markdown::{Markdown, find_testable_code, reset_headers};
|
||||
use html::markdown::{MarkdownWithToc, find_testable_code, reset_headers};
|
||||
use test::Collector;
|
||||
|
||||
fn load_string(input: &Path) -> io::IoResult<Option<~str>> {
|
||||
|
@ -145,7 +145,7 @@ pub fn render(input: &str, mut output: Path, matches: &getopts::Matches) -> int
|
|||
css = css,
|
||||
in_header = in_header,
|
||||
before_content = before_content,
|
||||
text = Markdown(text),
|
||||
text = MarkdownWithToc(text),
|
||||
after_content = after_content);
|
||||
|
||||
match err {
|
||||
|
|
Loading…
Reference in New Issue