]> git.lizzy.rs Git - rust.git/commitdiff
rustdoc: add table-of-contents recording & rendering, use it with plain
authorHuon Wilson <dbau.pp+github@gmail.com>
Fri, 7 Mar 2014 14:13:17 +0000 (01:13 +1100)
committerHuon Wilson <dbau.pp+github@gmail.com>
Sun, 9 Mar 2014 08:29:49 +0000 (19:29 +1100)
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).

src/librustdoc/html/markdown.rs
src/librustdoc/html/toc.rs [new file with mode: 0644]
src/librustdoc/lib.rs
src/librustdoc/markdown.rs

index 30040a1846c3866c28909a0376fd6b5e972f4e2b..8f7829dda9d1d693b6f645fbea6d1a8915eaa42d 100644 (file)
 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 @@ fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
         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)
     }
 }
diff --git a/src/librustdoc/html/toc.rs b/src/librustdoc/html/toc.rs
new file mode 100644 (file)
index 0000000..61031c2
--- /dev/null
@@ -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());
+    }
+}
index 94bc5ed2526630f4757d10e6295df5bdab363ca0..2d08dca97b986af22d987226a7f18d316474dfe0 100644 (file)
@@ -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;
index a998e3d69944f797e7a02fcc186ac6fe0da578c8..67a08706e982c83a8403662cb4f26662ecfe4421 100644 (file)
@@ -18,7 +18,7 @@
 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 {