Data formats

Markdown TOC

Tables of contents for Markdown: headings as a list or tree with GitHub-style anchors, a ready TOC, and HTML with heading ids.

Version 1.0.0 · 4 functions · 1 crate

#markdown#docs#text#navigation

Functions

What use markdown_toc.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use toc_headings, toc_markdown from markdown_toc (Quillo 0.6).

toc_headings(markdown: str) → listor an error

The headings of markdown, in order: { level, text, anchor, line }. anchor is the id GitHub (and toc_html) gives the heading: ## Getting started is getting-started, a second one getting-started-1; ## Setup {#install} keeps install. line counts from 1. Headings inside code blocks are not headings.

toc_markdown(markdown: str, min_level: int, max_level: int) → stror an error

A table of contents in Markdown, a nested list of links to the headings from level min_level to max_level (1 to 6; 2 and 3 skip the page's title and keep it short): - [Getting started](#getting-started).

toc_tree(markdown: str) → listor an error

The headings as a tree: { level, text, anchor, children }, each heading under the nearest one above it of a smaller level.

toc_html(markdown: str) → stror an error

The page as HTML with each heading's id set to its anchor, so the table of contents' links work. HTML written inside the Markdown is shown as text, not run, so the page is safe for what visitors write; tables, task lists, strikethrough and footnotes are on.

Get it

$ quillo install markdown_toc               # markdown_toc.qwer into this folder$ quillo install markdown_toc --example     # and its example, markdown_toc.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/markdown_toc.qwer$ curl -O https://quillolang.org/plugins/files/markdown_toc.qwe$ curl -O https://quillolang.org/plugins/files/markdown_toc_sample.md

Then, in your program, all of it or only what you call:

main.qwe
use markdown_toc.qweruse toc_headings, toc_markdown from markdown_toc      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }

Example

markdown_toc.qwe
// A documentation page with its table of contents beside it, and links that jump to each heading.////   quillo run markdown_toc.qwe      then open http://localhost:8080/docsuse "markdown_toc.qwer"let DOC = read_file("markdown_toc_sample.md")?route GET /docs {    let toc = toc_markdown(DOC, 2, 3)?    respond(200, `<nav>${str(markdown(toc))}</nav><main>${toc_html(DOC)?}</main>`, { "content-type": "text/html; charset=utf-8" })}route GET /toc = { markdown: toc_markdown(DOC, 2, 3)?, headings: toc_headings(DOC)? | map((h) => `${h.level} ${h.anchor} (line ${h.line})`) }route GET /tree = toc_tree(DOC)?
$ quillo run markdown_toc.qwe$ curl 'localhost:8080/docs'$ curl 'localhost:8080/toc'

Source

markdown_toc.qwer
//! Tables of contents for Markdown: the headings as a list or a tree with GitHub-style anchors, a ready-made TOC in Markdown, and the page's HTML with those ids on its headings.//! version: 1.0.0//!//! [dependencies]//! pulldown-cmark = { version = "0.12", default-features = false, features = ["html"] }use pulldown_cmark::{CowStr, Event, HeadingLevel, Options, Parser, Tag, TagEnd};use serde_json::{json, Value};use std::collections::HashMap;/// Texts up to 10 MB are read.const MAX: usize = 10 * 1024 * 1024;struct Heading {    level: usize,    text: String,    anchor: String,    line: usize,}fn options() -> Options {    Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH | Options::ENABLE_TASKLISTS | Options::ENABLE_HEADING_ATTRIBUTES | Options::ENABLE_FOOTNOTES}fn level(l: HeadingLevel) -> usize {    match l {        HeadingLevel::H1 => 1,        HeadingLevel::H2 => 2,        HeadingLevel::H3 => 3,        HeadingLevel::H4 => 4,        HeadingLevel::H5 => 5,        HeadingLevel::H6 => 6,    }}/// GitHub's anchor: lower case, punctuation dropped (but `-` and `_`), spaces as `-`.fn slug(text: &str) -> String {    text.trim().to_lowercase().chars().filter_map(|c| {        if c.is_alphanumeric() || c == '-' || c == '_' { Some(c) } else if c == ' ' { Some('-') } else { None }    }).collect()}fn headings(text: &str) -> Result<Vec<Heading>, String> {    if text.len() > MAX {        return Err("Markdown texts up to 10 MB".into());    }    let mut out = vec![];    let mut used: HashMap<String, usize> = HashMap::new();    let mut cur: Option<(usize, Option<String>, String, usize)> = None;    for (ev, range) in Parser::new_ext(text, options()).into_offset_iter() {        match ev {            Event::Start(Tag::Heading { level: l, id, .. }) => {                let line = text[..range.start].matches('\n').count() + 1;                cur = Some((level(l), id.map(|i| i.to_string()), String::new(), line));            }            Event::Text(t) | Event::Code(t) => {                if let Some(c) = cur.as_mut() {                    c.2.push_str(&t);                }            }            Event::End(TagEnd::Heading(_)) => {                if let Some((lvl, id, t, line)) = cur.take() {                    let base = id.unwrap_or_else(|| slug(&t));                    let n = used.entry(base.clone()).or_insert(0);                    let anchor = if *n == 0 { base.clone() } else { format!("{}-{}", base, n) };                    *n += 1;                    out.push(Heading { level: lvl, text: t.trim().to_string(), anchor, line });                }            }            _ => {}        }    }    Ok(out)}/// The headings of `markdown`, in order: `{ level, text, anchor, line }`. `anchor` is the id GitHub (and toc_html) gives the/// heading: `## Getting started` is `getting-started`, a second one `getting-started-1`; `## Setup {#install}` keeps `install`./// `line` counts from 1. Headings inside code blocks are not headings.pub fn toc_headings(markdown: &str) -> Result<Vec<Value>, String> {    Ok(headings(markdown)?.iter().map(|h| json!({ "level": h.level, "text": h.text, "anchor": h.anchor, "line": h.line })).collect())}/// A table of contents in Markdown, a nested list of links to the headings from level `min_level` to `max_level` (1 to 6;/// 2 and 3 skip the page's title and keep it short): `- [Getting started](#getting-started)`.pub fn toc_markdown(markdown: &str, min_level: i64, max_level: i64) -> Result<String, String> {    let (lo, hi) = (min_level.clamp(1, 6) as usize, max_level.clamp(1, 6) as usize);    let hs: Vec<Heading> = headings(markdown)?.into_iter().filter(|h| h.level >= lo && h.level <= hi).collect();    let top = hs.iter().map(|h| h.level).min().unwrap_or(lo);    let mut out = String::new();    for h in &hs {        let label = h.text.replace('[', "\\[").replace(']', "\\]");        out.push_str(&format!("{}- [{}](#{})\n", "  ".repeat(h.level - top), label, h.anchor));    }    Ok(out)}/// The headings as a tree: `{ level, text, anchor, children }`, each heading under the nearest one above it of a smaller level.pub fn toc_tree(markdown: &str) -> Result<Vec<Value>, String> {    fn take(hs: &[Heading], i: &mut usize, parent: usize) -> Vec<Value> {        let mut out = vec![];        while *i < hs.len() && hs[*i].level > parent {            let h = &hs[*i];            *i += 1;            let children = take(hs, i, h.level);            out.push(json!({ "level": h.level, "text": h.text, "anchor": h.anchor, "children": children }));        }        out    }    let hs = headings(markdown)?;    let mut i = 0;    Ok(take(&hs, &mut i, 0))}/// The page as HTML with each heading's `id` set to its anchor, so the table of contents' links work. HTML written inside/// the Markdown is shown as text, not run, so the page is safe for what visitors write; tables, task lists, strikethrough/// and footnotes are on.pub fn toc_html(markdown: &str) -> Result<String, String> {    let hs = headings(markdown)?;    let mut ids = hs.iter().map(|h| h.anchor.clone());    let events = Parser::new_ext(markdown, options()).map(|ev| match ev {        Event::Start(Tag::Heading { level, classes, attrs, .. }) => Event::Start(Tag::Heading { level, id: ids.next().map(CowStr::from), classes, attrs }),        Event::Html(h) | Event::InlineHtml(h) => Event::Text(h),        other => other,    });    let mut out = String::new();    pulldown_cmark::html::push_html(&mut out, events);    Ok(out)}
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On Markdown TOC: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.