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;
struct my_opaque {
opt: html_renderopt,
dfltblk: extern "C" fn(*buf, *buf, *buf, *libc::c_void),
+ toc_builder: Option<TocBuilder>,
}
struct buf {
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);
}
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) });
}
}).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();
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) });
}
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
}
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)
}
}
--- /dev/null
+// 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());
+ }
+}