Text

reStructuredText

reStructuredText to HTML with no Python: sections by adornment in the order they appear, nested lists, definition and field lists, literal blocks, grid tables with spans, simple, list and CSV tables, directives, roles, targets of every kind, footnotes and substitutions. Raw blocks and includes refused, and references that lead nowhere listed.

Version 1.0.0 · 3 functions · 1 crate

#markup#documentation#html#sphinx

Functions

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

rst_to_html(text: str) → str

reStructuredText as HTML. Raw blocks and includes are refused; javascript: links dropped; text is escaped.

rst_parse(text: str) → any

The document as data: { title, headings, html, problems }, with references that lead nowhere, short underlines, and unknown directives and roles among the problems.

rst_fields(text: str) → any

The field list at the top of a document, its bibliographic fields: :Author: Ada as { author: "Ada" }.

Get it

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

Or with curl:

$ curl -O https://quillolang.org/plugins/files/rst.qwer$ curl -O https://quillolang.org/plugins/files/rst.qwe$ curl -O https://quillolang.org/plugins/files/rst_sample.rst

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

main.qwe
use rst.qweruse rst_to_html, rst_parse from rst      // Quillo 0.6: only these

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

[dependencies]
serde_json = { version = "1", features = ["preserve_order"] }

Example

rst.qwe
// reStructuredText shown as a page, as a docs site would: the document, its table of contents,// the fields at its top, and a list of what does not resolve, for a build to fail on.////   quillo run rst.qwe      then open http://localhost:8080/page, /data, /fields and /smalluse "rst.qwer"let TEXT = read_file("rst_sample.rst")route GET /page = respond(200, rst_to_html(TEXT), { "content-type": "text/plain" })route GET /data {    let d = rst_parse(TEXT)    { title: d.title, headings: d.headings | map((h) => [h.level, h.title, h.id]), problems: d.problems }}route GET /fields = rst_fields(TEXT)route GET /small = {    transition: rst_to_html("a\n\n----\n\nb\n"),    empty: rst_to_html(""),    levels: rst_parse("A\n=\n\nB\n-\n\nC\n=\n\nD\n~\n\nE\n-\n").headings | map((h) => h.level),    duplicate: rst_parse("Same\n====\n\nSame\n====\n").headings | map((h) => h.id),    crlf: rst_to_html("Title\r\n=====\r\n\r\ntext\r\n"),    literal_end: rst_to_html("Words::\n\n  code\n\nafter\n"),    nested_inline: rst_to_html("*a* *b*, (*c*) d*e* *\n"),    bad_grid: rst_parse("+---+\n| a\n+---+\n").problems,    long: rst_to_html(repeat("word ", 3)),    self_naming: rst_parse(".. |a| replace:: x |a|\n\n|a|\n").problems,}
$ quillo run rst.qwe$ curl 'localhost:8080/page'$ curl 'localhost:8080/data'$ curl 'localhost:8080/fields'$ curl 'localhost:8080/small'

Source

rst.qwer
//! reStructuredText to HTML: sections by underline, directives, roles, grid and simple tables, field lists, targets, footnotes and substitutions, with what could not be resolved listed.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Map, Value};use std::collections::BTreeMap;const MAX_DEPTH: usize = 32;const MAX_SUB_DEPTH: usize = 8;const MAX_SUBS: usize = 20_000;const PUNCT: &str = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";const ADMONITIONS: [&str; 9] = ["attention", "caution", "danger", "error", "hint", "important", "note", "tip", "warning"];fn esc(s: &str) -> String {    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")}fn attr(s: &str) -> String {    esc(s).replace('"', "&quot;")}fn safe_url(u: &str) -> Option<String> {    let low: String = u.chars().filter(|c| !c.is_whitespace() && !c.is_control()).collect::<String>().to_lowercase();    if ["javascript:", "vbscript:", "data:text/html"].iter().any(|p| low.starts_with(p)) {        return None;    }    Some(u.trim().to_string())}fn norm(s: &str) -> String {    s.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase()}fn slug(t: &str) -> String {    let mut out = String::new();    let mut dash = false;    for c in t.to_lowercase().chars() {        if c.is_alphanumeric() {            if dash && !out.is_empty() {                out.push('-');            }            dash = false;            out.push(c);        } else {            dash = true;        }    }    if out.is_empty() { "section".to_string() } else { out }}fn indent_of(l: &str) -> usize {    l.chars().take_while(|c| *c == ' ').count()}fn is_adornment(l: &str) -> bool {    let t = l.trim_end();    if t.len() < 2 && t.len() != 1 {        return false;    }    let mut c = t.chars();    let Some(first) = c.next() else { return false };    PUNCT.contains(first) && t.chars().all(|x| x == first)}#[derive(Default)]struct Doc {    targets: BTreeMap<String, String>,    anon: Vec<String>,    anon_used: usize,    subs: BTreeMap<String, String>,    foot_defs: Vec<(String, String)>,    foot_refs: Vec<String>,    auto: usize,    manual: Vec<String>,    sub_depth: usize,    sub_used: usize,    levels: Vec<String>,    heads: Vec<Value>,    ids: Vec<String>,    problems: Vec<String>,    toc_wanted: bool,    title: Option<String>,}impl Doc {    /// The next automatic footnote number, past those written by hand.    fn next_auto(&mut self) -> String {        loop {            self.auto += 1;            let n = self.auto.to_string();            if !self.manual.contains(&n) {                return n;            }        }    }    fn unique(&mut self, base: String) -> String {        let mut n = base.clone();        let mut k = 1;        while self.ids.contains(&n) {            n = format!("{}-{}", base, k);            k += 1;        }        self.ids.push(n.clone());        n    }}/// First pass: targets, substitutions and footnotes anywhere can be used before they are defined.fn collect(lines: &[&str], d: &mut Doc) {    for l in lines {        if let Some((label, _)) = l.trim_start().strip_prefix(".. [").and_then(|r| r.split_once(']')) {            if label.bytes().all(|b| b.is_ascii_digit()) {                d.manual.push(label.to_string());            }        }    }    for (i, l) in lines.iter().enumerate() {        let t = l.trim_start();        if let Some(r) = t.strip_prefix(".. _").or(t.strip_prefix("__ ").map(|x| x)) {            if t.starts_with("__ ") {                d.anon.push(r.trim().to_string());                continue;            }            let (name, url) = match r.strip_prefix('`') {                Some(q) => match q.split_once("`:") {                    Some((a, b)) => (a, b),                    None => continue,                },                None => {                    let c: Vec<char> = r.chars().collect();                    let mut k = 0;                    while k < c.len() && !(c[k] == ':' && (k + 1 == c.len() || c[k + 1] == ' ') && (k == 0 || c[k - 1] != '\\')) {                        k += 1;                    }                    if k >= c.len() {                        continue;                    }                    let n: String = c[..k].iter().collect();                    let rest: String = c[k + 1..].iter().collect();                    d.targets.insert(norm(&n.replace("\\:", ":")), glue_url(&rest, lines, i));                    continue;                }            };            d.targets.insert(norm(name), glue_url(url, lines, i));        } else if let Some(r) = t.strip_prefix(".. __:") {            d.anon.push(r.trim().to_string());        } else if let Some(r) = t.strip_prefix(".. |") {            if let Some((name, def)) = r.split_once("| replace::") {                let mut text = def.trim().to_string();                let mut k = i + 1;                while k < lines.len() && indent_of(lines[k]) > 0 && !lines[k].trim().is_empty() {                    text.push(' ');                    text.push_str(lines[k].trim());                    k += 1;                }                d.subs.insert(name.to_string(), format!("r{}", text));            } else if let Some((name, def)) = r.split_once("| image::") {                d.subs.insert(name.to_string(), format!("i{}", def.trim()));            } else if let Some((name, def)) = r.split_once("| unicode::") {                let mut s = String::new();                for w in def.split_whitespace() {                    let hex = w.strip_prefix("0x").or(w.strip_prefix("U+")).or(w.strip_prefix("u+"));                    match hex.and_then(|h| u32::from_str_radix(h, 16).ok()).and_then(char::from_u32) {                        Some(c) => s.push(c),                        None => {                            s.push_str(w);                            s.push(' ');                        }                    }                }                d.subs.insert(name.to_string(), format!("r{}", s.trim()));            }        } else if let Some(r) = t.strip_prefix(".. [") {            if let Some((label, _)) = r.split_once(']') {                let mut text = r[label.len() + 1..].trim().to_string();                let pad = indent_of(l);                let mut k = i + 1;                while k < lines.len() && !lines[k].trim().is_empty() && indent_of(lines[k]) > pad {                    text.push(' ');                    text.push_str(lines[k].trim());                    k += 1;                }                let key = if label == "#" || label == "*" {                    d.next_auto()                } else if let Some(n) = label.strip_prefix('#') {                    let k = d.next_auto();                    d.targets.insert(format!("#{}", norm(n)), k.clone());                    k                } else {                    label.to_string()                };                d.foot_defs.push((key, text));            }        }    }    d.auto = 0;}/// A URL may go on over indented lines, with the spaces dropped.fn glue_url(first: &str, lines: &[&str], i: usize) -> String {    let mut u = first.trim().to_string();    let pad = indent_of(lines[i]);    let mut k = i + 1;    while k < lines.len() && !lines[k].trim().is_empty() && indent_of(lines[k]) > pad {        u.push_str(lines[k].trim());        k += 1;    }    u}fn start_ok(c: &[char], i: usize) -> bool {    i == 0 || c[i - 1].is_whitespace() || "'\"([{<-/:".contains(c[i - 1])}fn end_ok(c: &[char], i: usize) -> bool {    i >= c.len() || c[i].is_whitespace() || "'\")]}>-/:.,;!?\\".contains(c[i])}/// The closing `mark` at or after `from`: not after a space, not escaped, and followed by what may end inline markup.fn close_at(c: &[char], from: usize, mark: &str) -> Option<usize> {    let m: Vec<char> = mark.chars().collect();    let mut i = from;    while i + m.len() <= c.len() {        if c[i] == '\\' {            i += 2;            continue;        }        if c[i..].starts_with(&m) && i > from && !c[i - 1].is_whitespace() && end_ok(c, i + m.len()) {            return Some(i);        }        i += 1;    }    None}/// The closing backquote of `phrase`, which an underscore may follow.fn close_tick(c: &[char], from: usize) -> Option<usize> {    let mut i = from;    while i < c.len() {        if c[i] == '\\' {            i += 2;            continue;        }        if c[i] == '`' && i > from && !c[i - 1].is_whitespace() {            let after = c[i + 1..].iter().position(|x| *x != '_').map_or(c.len(), |p| i + 1 + p);            if end_ok(c, after) {                return Some(i);            }        }        i += 1;    }    None}fn unescape(s: &str) -> String {    let c: Vec<char> = s.chars().collect();    let mut o = String::new();    let mut i = 0;    while i < c.len() {        if c[i] == '\\' && i + 1 < c.len() {            i += 1;            if !c[i].is_whitespace() {                o.push(c[i]);            }        } else {            o.push(c[i]);        }        i += 1;    }    o}fn anchor(url: &str, text: &str, d: &mut Doc) -> String {    match safe_url(url) {        Some(u) => format!("<a href=\"{}\">{}</a>", attr(&u), text),        None => {            d.problems.push(format!("the link {} was dropped", url));            text.to_string()        }    }}fn reference(name: &str, shown: &str, d: &mut Doc, anon: bool) -> String {    if anon {        d.anon_used += 1;        return match d.anon.get(d.anon_used - 1).cloned() {            Some(u) => anchor(&u, shown, d),            None => {                d.problems.push(format!("no anonymous target for `{}`", name));                shown.to_string()            }        };    }    let key = norm(name);    match d.targets.get(&key).cloned() {        Some(u) if u.ends_with('_') && !u.contains(' ') => {            // An indirect target: a name that points at another.            let next = u.trim_end_matches('_').to_string();            match d.targets.get(&norm(&next)).cloned() {                Some(v) => anchor(&v, shown, d),                None => {                    d.problems.push(format!("the target {} is not defined", next));                    shown.to_string()                }            }        }        Some(u) if !u.is_empty() => anchor(&u, shown, d),        // An internal target, or a section by its title.        Some(_) => format!("<a href=\"#{}\">{}</a>", attr(&slug(name)), shown),        None if d.ids.contains(&slug(name)) || d.heads.iter().any(|h| h["title"].as_str().map(norm).as_deref() == Some(&key)) => format!("<a href=\"#{}\">{}</a>", attr(&slug(name)), shown),        None => {            d.problems.push(format!("the reference {} has no target", name));            format!("<span class=\"problem\">{}</span>", shown)        }    }}fn role(name: &str, body: &str, d: &mut Doc) -> String {    let t = esc(&unescape(body));    match name {        "emphasis" => format!("<em>{}</em>", t),        "strong" => format!("<strong>{}</strong>", t),        "literal" | "code" => format!("<code>{}</code>", t),        "sub" | "subscript" => format!("<sub>{}</sub>", t),        "sup" | "superscript" => format!("<sup>{}</sup>", t),        "title" | "title-reference" | "t" => format!("<cite>{}</cite>", t),        "math" => format!("<span class=\"math\">{}</span>", t),        "abbreviation" | "ab" | "acronym" | "ac" => match body.rsplit_once(" <") {            Some((a, b)) => format!("<abbr title=\"{}\">{}</abbr>", attr(b.trim_end_matches('>')), esc(a)),            None => format!("<abbr>{}</abbr>", t),        },        "pep" => format!("<a href=\"https://peps.python.org/pep-{:04}/\">PEP {}</a>", body.trim().parse::<u32>().unwrap_or(0), t),        "rfc" => format!("<a href=\"https://www.rfc-editor.org/rfc/rfc{}\">RFC {}</a>", attr(body.trim()), t),        "ref" | "doc" => match body.rsplit_once(" <") {            Some((a, b)) => reference(b.trim_end_matches('>'), &esc(a), d, false),            None => reference(body, &t, d, false),        },        o => {            d.problems.push(format!("the role {} is not known", o));            format!("<span class=\"role-{}\">{}</span>", attr(o), t)        }    }}fn inline(text: &str, d: &mut Doc) -> String {    let c: Vec<char> = text.chars().collect();    let mut o = String::new();    let mut i = 0;    while i < c.len() {        let x = c[i];        if x == '\\' && i + 1 < c.len() {            i += 1;            if !c[i].is_whitespace() {                o.push_str(&esc(&c[i].to_string()));            }            i += 1;            continue;        }        let can = start_ok(&c, i);        if can && c[i..].starts_with(&['`', '`']) && c.get(i + 2).map_or(false, |n| !n.is_whitespace()) {            // Literals have no escapes inside.            let mut k = i + 3;            let mut hit = None;            while k + 1 < c.len() {                if c[k] == '`' && c[k + 1] == '`' && !c[k - 1].is_whitespace() && end_ok(&c, k + 2) {                    hit = Some(k);                    break;                }                k += 1;            }            if let Some(e) = hit {                o.push_str(&format!("<code>{}</code>", esc(&c[i + 2..e].iter().collect::<String>())));                i = e + 2;                continue;            }        }        if can && c[i..].starts_with(&['*', '*']) && c.get(i + 2).map_or(false, |n| !n.is_whitespace()) {            if let Some(e) = close_at(&c, i + 2, "**") {                o.push_str(&format!("<strong>{}</strong>", esc(&unescape(&c[i + 2..e].iter().collect::<String>()))));                i = e + 2;                continue;            }        }        if can && x == '*' && c.get(i + 1).map_or(false, |n| !n.is_whitespace() && *n != '*') {            if let Some(e) = close_at(&c, i + 1, "*") {                o.push_str(&format!("<em>{}</em>", esc(&unescape(&c[i + 1..e].iter().collect::<String>()))));                i = e + 1;                continue;            }        }        if can && x == ':' {            // :role:`text`            let mut k = i + 1;            while k < c.len() && (c[k].is_alphanumeric() || "-_+.".contains(c[k]) || (c[k] == ':' && c.get(k + 1) != Some(&'`'))) {                k += 1;            }            if k > i + 1 && c.get(k) == Some(&':') && c.get(k + 1) == Some(&'`') {                if let Some(e) = close_at(&c, k + 2, "`") {                    let name: String = c[i + 1..k].iter().collect();                    let body: String = c[k + 2..e].iter().collect();                    o.push_str(&role(&name, &body, d));                    i = e + 1;                    continue;                }            }        }        if can && x == '_' && c.get(i + 1) == Some(&'`') {            if let Some(e) = close_at(&c, i + 2, "`") {                let name: String = c[i + 2..e].iter().collect();                let id = d.unique(slug(&name));                o.push_str(&format!("<span id=\"{}\">{}</span>", attr(&id), esc(&name)));                d.targets.entry(norm(&name)).or_default();                i = e + 1;                continue;            }        }        if can && x == '`' {            if let Some(e) = close_tick(&c, i + 1) {                // Found with its trailing underscores looked at: close_at is lenient for `_`.                let body: String = c[i + 1..e].iter().collect();                let under = c[e + 1..].iter().take_while(|y| **y == '_').count();                if under > 0 {                    let anon = under >= 2;                    let next = e + 1 + under;                    // `text <url>`_                    if let Some((label, inner)) = body.trim_end_matches('>').rsplit_once('<').filter(|_| body.ends_with('>')) {                        let inner = inner.trim();                        let shown = esc(&unescape(label.trim()));                        let html = if let Some(n) = inner.strip_suffix('_').filter(|n| !n.contains('/')) {                            reference(n, &shown, d, false)                        } else {                            let u: String = inner.split_whitespace().collect();                            if !anon {                                d.targets.entry(norm(label)).or_insert(u.clone());                            }                            anchor(&u, &shown, d)                        };                        o.push_str(&html);                    } else {                        o.push_str(&reference(&unescape(&body), &esc(&unescape(&body)), d, anon));                    }                    i = next;                    continue;                }                // The default role.                o.push_str(&format!("<cite>{}</cite>", esc(&unescape(&body))));                i = e + 1;                continue;            }        }        if can && x == '|' {            if let Some(e) = close_at(&c, i + 1, "|") {                let name: String = c[i + 1..e].iter().collect();                let under = c[e + 1..].iter().take_while(|y| **y == '_').count();                d.sub_used += 1;                let html = match d.subs.get(&name).cloned() {                    // A substitution may name itself, or double at each step.                    Some(_) if d.sub_depth >= MAX_SUB_DEPTH || d.sub_used > MAX_SUBS => {                        d.problems.push(format!("the substitution |{}| goes too deep", name));                        esc(&format!("|{}|", name))                    }                    Some(v) if v.starts_with('r') => {                        d.sub_depth += 1;                        let h = inline(&v[1..], d);                        d.sub_depth -= 1;                        h                    }                    Some(v) => format!("<img src=\"{}\" alt=\"{}\">", attr(&safe_url(&v[1..]).unwrap_or_default()), attr(&name)),                    None => {                        d.problems.push(format!("the substitution |{}| is not defined", name));                        esc(&format!("|{}|", name))                    }                };                let html = if under > 0 { reference(&name, &html, d, under > 1) } else { html };                o.push_str(&html);                i = e + 1 + under;                continue;            }        }        if can && x == '[' {            if let Some(e) = c[i..].iter().position(|y| *y == ']').map(|p| p + i) {                if c.get(e + 1) == Some(&'_') {                    let label: String = c[i + 1..e].iter().collect();                    let key = if label == "#" || label == "*" {                        Some(d.next_auto())                    } else if let Some(n) = label.strip_prefix('#') {                        d.targets.get(&format!("#{}", norm(n))).cloned()                    } else if !label.is_empty() {                        Some(label.clone())                    } else {                        None                    };                    if let Some(k) = key {                        if d.foot_defs.iter().any(|f| f.0 == k) {                            if !d.foot_refs.contains(&k) {                                d.foot_refs.push(k.clone());                            }                            o.push_str(&format!("<sup class=\"footnote\"><a href=\"#fn-{0}\">{1}</a></sup>", attr(&k), esc(&k)));                        } else {                            d.problems.push(format!("the footnote [{}] is not defined", k));                            o.push_str(&esc(&format!("[{}]", k)));                        }                        i = e + 2;                        continue;                    }                }            }        }        // name_ and name__, and bare URLs.        if can && (x.is_alphanumeric()) {            let mut k = i;            while k < c.len() && (c[k].is_alphanumeric() || ((c[k] == '_' || c[k] == '-' || c[k] == '.' || c[k] == '+' || c[k] == ':') && c.get(k + 1).map_or(false, |n| n.is_alphanumeric()))) {                k += 1;            }            let word: String = c[i..k].iter().collect();            if c.get(k) == Some(&'_') {                let under = c[k..].iter().take_while(|y| **y == '_').count();                if end_ok(&c, k + under) && under <= 2 {                    o.push_str(&reference(&word, &esc(&word), d, under == 2));                    i = k + under;                    continue;                }            }            if ["http://", "https://", "ftp://", "mailto:"].iter().any(|s| c[i..].iter().take(s.len()).collect::<String>() == *s) {                let mut e = i;                while e < c.len() && !c[e].is_whitespace() && !"<>\"".contains(c[e]) {                    e += 1;                }                while e > i && ".,;:!?')".contains(c[e - 1]) {                    e -= 1;                }                let u: String = c[i..e].iter().collect();                o.push_str(&anchor(&u, &esc(&u), d));                i = e;                continue;            }        }        o.push_str(&esc(&x.to_string()));        i += 1;    }    o}fn bullet(l: &str) -> Option<&str> {    let t = l.trim_start();    let mut c = t.chars();    let m = c.next()?;    if "*-+•‣⁃".contains(m) && (c.next() == Some(' ') || t.len() == m.len_utf8()) {        return Some(t[m.len_utf8()..].trim_start());    }    None}/// `1.`, `(a)`, `#.`, `iv)`: `(the marker, kind, the start number, text offset)`.fn enumerator(l: &str) -> Option<(String, char, i64, usize)> {    let t = l.trim_start();    let pad = l.len() - t.len();    let (open, rest) = t.strip_prefix('(').map_or((false, t), |r| (true, r));    let n = rest.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '#').count();    if n == 0 {        return None;    }    let (word, tail) = rest.split_at(n);    let close = tail.chars().next()?;    if open && close != ')' || !open && close != '.' && close != ')' {        return None;    }    if !(tail.len() == 1 || tail[1..].starts_with(' ')) {        return None;    }    let (kind, start) = if word == "#" {        ('#', 1)    } else if word.bytes().all(|b| b.is_ascii_digit()) {        ('1', word.parse().ok()?)    } else if word.len() == 1 && word.as_bytes()[0].is_ascii_lowercase() {        ('a', (word.as_bytes()[0] - b'a') as i64 + 1)    } else if word.len() == 1 && word.as_bytes()[0].is_ascii_uppercase() {        ('A', (word.as_bytes()[0] - b'A') as i64 + 1)    } else if word.bytes().all(|b| b"ivxlcdm".contains(&b)) {        ('i', roman(word)?)    } else if word.bytes().all(|b| b"IVXLCDM".contains(&b)) {        ('I', roman(&word.to_lowercase())?)    } else {        return None;    };    let off = pad + usize::from(open) + n + 1;    let text_at = off + t[usize::from(open) + n + 1..].chars().take_while(|c| *c == ' ').count();    Some((format!("{}{}", if open { "(" } else { "" }, close), kind, start, text_at))}fn roman(s: &str) -> Option<i64> {    let v = |c: char| match c { 'i' => 1, 'v' => 5, 'x' => 10, 'l' => 50, 'c' => 100, 'd' => 500, 'm' => 1000, _ => 0 };    let c: Vec<char> = s.chars().collect();    let mut t = 0;    for i in 0..c.len() {        if i + 1 < c.len() && v(c[i]) < v(c[i + 1]) { t -= v(c[i]) } else { t += v(c[i]) }    }    (t > 0).then_some(t)}fn field(l: &str) -> Option<(String, String)> {    let t = l.trim_start();    let r = t.strip_prefix(':')?;    let c: Vec<char> = r.chars().collect();    let mut k = 0;    while k < c.len() {        if c[k] == '\\' {            k += 2;            continue;        }        if c[k] == ':' && (k + 1 == c.len() || c[k + 1] == ' ') {            if k == 0 {                return None;            }            return Some((unescape(&c[..k].iter().collect::<String>()), c[k + 1..].iter().collect::<String>().trim().to_string()));        }        k += 1;    }    None}/// The lines under a marker, indent taken off: stops at the first line less indented than `pad`.fn indented<'a>(lines: &[&'a str], i: &mut usize, pad: usize) -> Vec<String> {    let mut out: Vec<String> = vec![];    while *i < lines.len() {        let l = lines[*i];        if l.trim().is_empty() {            out.push(String::new());            *i += 1;            continue;        }        if indent_of(l) < pad {            break;        }        out.push(l[pad..].to_string());        *i += 1;    }    while out.last().map_or(false, |x| x.is_empty()) {        out.pop();    }    out}fn dedent(lines: &[String]) -> Vec<String> {    let min = lines.iter().filter(|l| !l.trim().is_empty()).map(|l| indent_of(l)).min().unwrap_or(0);    lines.iter().map(|l| l.get(min..).unwrap_or("").to_string()).collect()}fn sub_blocks(lines: &[String], d: &mut Doc, depth: usize) -> String {    let refs: Vec<&str> = lines.iter().map(String::as_str).collect();    blocks(&refs, d, depth + 1)}fn grid_table(lines: &[String], d: &mut Doc) -> Option<String> {    let g: Vec<Vec<char>> = lines.iter().map(|l| l.trim_end().chars().collect()).collect();    let (h, w) = (g.len(), g.first()?.len());    if h < 3 || g.iter().any(|r| r.len() != w) {        return None;    }    let head_at = g.iter().position(|r| r.iter().all(|c| *c == '+' || *c == '=') && r.contains(&'='));    let is_corner = |r: usize, c: usize| g[r][c] == '+';    let mut done: Vec<(usize, usize)> = vec![];    let mut todo = vec![(0usize, 0usize)];    let mut cells: Vec<(usize, usize, usize, usize)> = vec![];    while let Some((r, c)) = todo.pop() {        if r >= h - 1 || c >= w - 1 || done.contains(&(r, c)) {            continue;        }        done.push((r, c));        // Right along the top to each corner, down its side to a corner that closes the box.        let mut found = None;        let mut right = c + 1;        'scan: while right < w {            if g[r][right] != '-' && g[r][right] != '=' && !is_corner(r, right) {                break;            }            if is_corner(r, right) {                let mut down = r + 1;                while down < h {                    if g[down][right] != '|' && !is_corner(down, right) {                        break;                    }                    if is_corner(down, right) {                        let bottom_ok = (c..=right).all(|k| "-=+".contains(g[down][k]));                        let left_ok = (r..=down).all(|k| "|+".contains(g[k][c]));                        if bottom_ok && left_ok {                            found = Some((right, down));                            break 'scan;                        }                    }                    down += 1;                }            }            right += 1;        }        // A corner where a spanning cell ends starts nothing.        let Some((right, down)) = found else { continue };        cells.push((r, c, down, right));        todo.push((r, right));        todo.push((down, c));    }    if cells.iter().map(|x| (x.2 - x.0) * (x.3 - x.1)).sum::<usize>() != (h - 1) * (w - 1) {        return None;    }    let mut rows_at: Vec<usize> = cells.iter().map(|x| x.0).collect();    rows_at.sort();    rows_at.dedup();    let mut cols_at: Vec<usize> = cells.iter().flat_map(|x| [x.1, x.3]).collect();    cols_at.sort();    cols_at.dedup();    let row_ends: Vec<usize> = {        let mut v: Vec<usize> = cells.iter().flat_map(|x| [x.0, x.2]).collect();        v.sort();        v.dedup();        v    };    cells.sort();    let mut out = String::from("<table>\n");    let head_end = head_at;    let mut i = 0;    let mut in_head = head_end.is_some();    out.push_str(if in_head { "<thead>\n" } else { "<tbody>\n" });    for r in &rows_at {        if let Some(e) = head_end {            if *r >= e && in_head {                out.push_str("</thead>\n<tbody>\n");                in_head = false;            }        }        out.push_str("<tr>");        while i < cells.len() && cells[i].0 == *r {            let (t, l, b, ri) = cells[i];            i += 1;            let body: Vec<String> = (t + 1..b).map(|y| g[y][l + 1..ri].iter().collect::<String>().trim_end().to_string()).collect();            let body = dedent(&body);            let colspan = cols_at.iter().filter(|x| **x > l && **x <= ri).count();            let rowspan = row_ends.iter().filter(|x| **x > t && **x <= b).count();            let mut a = String::new();            if colspan > 1 {                a.push_str(&format!(" colspan=\"{}\"", colspan));            }            if rowspan > 1 {                a.push_str(&format!(" rowspan=\"{}\"", rowspan));            }            let tag = if in_head { "th" } else { "td" };            let html = sub_blocks(&body, d, 0);            let html = strip_p(&html);            out.push_str(&format!("<{0}{1}>{2}</{0}>", tag, a, html));        }        out.push_str("</tr>\n");    }    out.push_str(if in_head { "</thead>\n</table>\n" } else { "</tbody>\n</table>\n" });    Some(out)}/// One paragraph in a cell needs no `<p>`.fn strip_p(h: &str) -> String {    let t = h.trim();    if t.starts_with("<p>") && t.ends_with("</p>") && t.matches("<p>").count() == 1 {        return t[3..t.len() - 4].to_string();    }    t.to_string()}fn simple_table(lines: &[String], d: &mut Doc) -> Option<String> {    let rule = |l: &str| !l.trim().is_empty() && l.trim_end().chars().all(|c| c == '=' || c == ' ');    let first = &lines[0];    let mut cols: Vec<(usize, usize)> = vec![];    let c: Vec<char> = first.chars().collect();    let mut k = 0;    while k < c.len() {        if c[k] == '=' {            let s = k;            while k < c.len() && c[k] == '=' {                k += 1;            }            cols.push((s, k));        } else {            k += 1;        }    }    if cols.len() < 2 {        return None;    }    let rules: Vec<usize> = lines.iter().enumerate().filter(|(_, l)| rule(l)).map(|x| x.0).collect();    if rules.len() < 2 {        return None;    }    let cell = |l: &str, n: usize| -> String {        let ch: Vec<char> = l.chars().collect();        let (s, e) = cols[n];        let e = if n + 1 == cols.len() { ch.len() } else { e.min(ch.len()) };        if s >= ch.len() { String::new() } else { ch[s..e].iter().collect::<String>().trim().to_string() }    };    let mut rows = |from: usize, to: usize| -> Vec<Vec<String>> {        let mut out: Vec<Vec<String>> = vec![];        for l in &lines[from..to] {            if l.trim().is_empty() {                continue;            }            let vals: Vec<String> = (0..cols.len()).map(|n| cell(l, n)).collect();            // A blank first column goes on from the row above.            if vals[0].is_empty() && !out.is_empty() {                let last = out.last_mut().unwrap();                for (n, v) in vals.into_iter().enumerate() {                    if !v.is_empty() {                        last[n].push(' ');                        last[n].push_str(&v);                    }                }            } else {                out.push(vals);            }        }        out    };    let has_head = rules.len() >= 3;    let head = if has_head { rows(rules[0] + 1, rules[1]) } else { vec![] };    let body = rows(rules[rules.len() - 2] + 1, rules[rules.len() - 1]);    let mut out = String::from("<table>\n");    if has_head {        out.push_str("<thead>\n");        for r in &head {            out.push_str("<tr>");            for v in r {                out.push_str(&format!("<th>{}</th>", inline(v, d)));            }            out.push_str("</tr>\n");        }        out.push_str("</thead>\n");    }    out.push_str("<tbody>\n");    for r in &body {        out.push_str("<tr>");        for v in r {            out.push_str(&format!("<td>{}</td>", inline(v, d)));        }        out.push_str("</tr>\n");    }    out.push_str("</tbody>\n</table>\n");    Some(out)}fn csv_row(l: &str) -> Vec<String> {    let mut out = vec![];    let mut cur = String::new();    let mut q = false;    let c: Vec<char> = l.chars().collect();    let mut i = 0;    while i < c.len() {        match c[i] {            '"' if q && c.get(i + 1) == Some(&'"') => {                cur.push('"');                i += 1;            }            '"' => q = !q,            ',' if !q => out.push(std::mem::take(&mut cur).trim().to_string()),            x => cur.push(x),        }        i += 1;    }    out.push(cur.trim().to_string());    out}fn directive(name: &str, arg: &str, opts: &BTreeMap<String, String>, body: &[String], d: &mut Doc, depth: usize) -> String {    let id_class = |d: &mut Doc| -> String {        let mut s = String::new();        if let Some(n) = opts.get("name") {            let id = d.unique(slug(n));            d.targets.entry(norm(n)).or_default();            s.push_str(&format!(" id=\"{}\"", attr(&id)));        }        s    };    if ADMONITIONS.contains(&name) {        let mut content: Vec<String> = vec![];        if !arg.is_empty() {            content.push(arg.to_string());            content.push(String::new());        }        content.extend_from_slice(body);        let label = format!("{}{}", name[..1].to_uppercase(), &name[1..]);        return format!("<div class=\"admonition {}\">\n<p class=\"admonition-title\">{}</p>\n{}</div>\n", name, label, sub_blocks(&content, d, depth));    }    match name {        "admonition" => format!("<div class=\"admonition\">\n<p class=\"admonition-title\">{}</p>\n{}</div>\n", inline(arg, d), sub_blocks(body, d, depth)),        "code" | "code-block" | "sourcecode" => {            let lang = if arg.is_empty() { String::new() } else { format!(" class=\"language-{}\"", attr(arg)) };            let numbered = opts.contains_key("number-lines") || opts.contains_key("linenos");            let text = if numbered {                body.iter().enumerate().map(|(i, l)| format!("{:>3}  {}", i + 1, l)).collect::<Vec<_>>().join("\n")            } else {                body.join("\n")            };            let i = id_class(d);            format!("<pre{}><code{}>{}</code></pre>\n", i, lang, esc(&text))        }        "image" | "figure" => {            let Some(src) = safe_url(arg) else {                d.problems.push(format!("the image {} was dropped", arg));                return String::new();            };            let mut img = format!("<img src=\"{}\" alt=\"{}\"", attr(&src), attr(opts.get("alt").map_or("", |s| s.as_str())));            for k in ["width", "height"] {                if let Some(v) = opts.get(k) {                    img.push_str(&format!(" {}=\"{}\"", k, attr(v)));                }            }            img.push('>');            if let Some(t) = opts.get("target") {                img = anchor(t, &img, d);            }            if name == "image" {                return format!("<p>{}</p>\n", img);            }            let parts: Vec<&[String]> = body.splitn(2, |l| l.is_empty()).collect();            let cap = parts.first().map_or(String::new(), |p| inline(&p.join(" "), d));            format!("<figure>{}<figcaption>{}</figcaption></figure>\n", img, cap)        }        "contents" => {            d.toc_wanted = true;            format!("\u{3}TOC{}\u{3}\n", attr(if arg.is_empty() { "Contents" } else { arg }))        }        "raw" => {            d.problems.push(format!("a raw {} block was refused", arg));            format!("<pre class=\"raw-refused\">{}</pre>\n", esc(&body.join("\n")))        }        "include" => {            d.problems.push(format!("the include {} was refused", arg));            format!("<p class=\"include-refused\">include of {} refused</p>\n", esc(arg))        }        "topic" | "sidebar" => format!("<aside class=\"{}\">\n<p class=\"title\">{}</p>\n{}</aside>\n", name, inline(arg, d), sub_blocks(body, d, depth)),        "rubric" => format!("<p class=\"rubric\">{}</p>\n", inline(arg, d)),        "container" => format!("<div class=\"{}\">\n{}</div>\n", attr(arg), sub_blocks(body, d, depth)),        "parsed-literal" => format!("<pre class=\"literal\">{}</pre>\n", body.iter().map(|l| inline(l, d)).collect::<Vec<_>>().join("\n")),        "epigraph" | "highlights" | "pull-quote" => format!("<blockquote class=\"{}\">\n{}</blockquote>\n", name, sub_blocks(body, d, depth)),        "math" => format!("<div class=\"math\">{}</div>\n", esc(&if arg.is_empty() { body.join("\n") } else { arg.to_string() })),        "list-table" => {            let head = opts.get("header-rows").and_then(|h| h.parse::<usize>().ok()).unwrap_or(0);            // A list of lists: outer items are rows.            let refs: Vec<&str> = body.iter().map(String::as_str).collect();            let mut rows: Vec<Vec<String>> = vec![];            let mut i = 0;            while i < refs.len() {                if refs[i].trim().is_empty() {                    i += 1;                    continue;                }                if indent_of(refs[i]) == 0 && bullet(refs[i]).is_some() {                    let mut row = vec![];                    let first = bullet(refs[i]).unwrap_or("").to_string();                    i += 1;                    let mut block = vec![first];                    block.extend(indented(&refs, &mut i, 2));                    for b in dedent(&block).iter() {                        if let Some(c) = bullet(b) {                            row.push(c.to_string());                        } else if let Some(l) = row.last_mut() {                            l.push(' ');                            l.push_str(b.trim());                        }                    }                    rows.push(row);                } else {                    i += 1;                }            }            let mut s = String::new();            if !arg.is_empty() {                s.push_str(&format!("<p class=\"title\">{}</p>\n", inline(arg, d)));            }            s.push_str("<table>\n");            for (n, r) in rows.iter().enumerate() {                let tag = if n < head { "th" } else { "td" };                s.push_str("<tr>");                for c in r {                    s.push_str(&format!("<{0}>{1}</{0}>", tag, inline(c, d)));                }                s.push_str("</tr>\n");            }            s.push_str("</table>\n");            s        }        "csv-table" => {            let mut s = String::from("<table>\n");            if let Some(h) = opts.get("header") {                s.push_str("<thead>\n<tr>");                for c in csv_row(h) {                    s.push_str(&format!("<th>{}</th>", inline(&c, d)));                }                s.push_str("</tr>\n</thead>\n");            }            s.push_str("<tbody>\n");            for l in body.iter().filter(|l| !l.trim().is_empty()) {                s.push_str("<tr>");                for c in csv_row(l) {                    s.push_str(&format!("<td>{}</td>", inline(&c, d)));                }                s.push_str("</tr>\n");            }            s.push_str("</tbody>\n</table>\n");            s        }        o => {            d.problems.push(format!("the directive {} is not known", o));            format!("<pre class=\"unknown-directive\">{}</pre>\n", esc(&format!(".. {}:: {}\n{}", o, arg, body.join("\n"))))        }    }}fn dir_parse(l: &str) -> Option<(String, String)> {    let r = l.trim_start().strip_prefix(".. ")?;    let (n, a) = r.split_once("::")?;    if n.is_empty() || !n.chars().all(|c| c.is_alphanumeric() || "-_+.:".contains(c)) {        return None;    }    (a.is_empty() || a.starts_with(' ')).then(|| (n.to_lowercase(), a.trim().to_string()))}fn blocks(lines: &[&str], d: &mut Doc, depth: usize) -> String {    if depth > MAX_DEPTH {        d.problems.push("blocks are nested too deep".to_string());        return String::new();    }    let mut o = String::new();    let mut i = 0;    while i < lines.len() {        let l = lines[i];        let t = l.trim_end();        if t.is_empty() {            i += 1;            continue;        }        let pad = indent_of(l);        // Sections: an underline, or an overline with a matching underline.        if pad == 0 && depth == 0 {            let next = lines.get(i + 1).map(|x| x.trim_end());            if is_adornment(t) && t.len() >= 4 && next.map_or(true, str::is_empty) {                o.push_str("<hr>\n");                i += 2;                continue;            }            let over = is_adornment(t) && next.map_or(false, |n| !n.is_empty()) && lines.get(i + 2).map_or(false, |u| is_adornment(u) && u.trim_end() == t);            let under = !is_adornment(t) && next.map_or(false, |n| is_adornment(n) && n.chars().count() >= 3.min(t.chars().count()));            if over || under {                let title = if over { lines[i + 1].trim() } else { t };                let rule = if over { lines[i + 2].trim_end() } else { next.unwrap_or("") };                if rule.chars().count() < title.chars().count() {                    d.problems.push(format!("line {}: the underline is shorter than `{}`", i + 2, title));                }                let key = format!("{}{}", if over { "o" } else { "u" }, &rule[..rule.chars().next().map_or(0, char::len_utf8)]);                let level = match d.levels.iter().position(|k| *k == key) {                    Some(p) => p,                    None => {                        d.levels.push(key);                        d.levels.len() - 1                    }                };                let html = inline(title, d);                let id = d.unique(slug(title));                d.heads.push(json!({ "level": level + 1, "title": title, "id": id }));                if d.title.is_none() && level == 0 {                    d.title = Some(title.to_string());                }                o.push_str(&format!("<h{0} id=\"{1}\">{2}</h{0}>\n", (level + 1).min(6), attr(&id), html));                i += if over { 3 } else { 2 };                continue;            }        }        // Explicit markup.        if t.trim_start().starts_with(".. ") || t.trim_start() == ".." {            let body_pad = pad + 3;            if let Some(r) = t.trim_start().strip_prefix(".. |") {                if r.contains("| ") {                    i += 1;                    let _ = indented(lines, &mut i, body_pad);                    continue;                }            }            if t.trim_start().starts_with(".. _") || t.trim_start().starts_with(".. __:") {                // A target: an anchor if it names nothing, for what follows.                let s = t.trim_start();                if s.ends_with(':') && !s.ends_with("::") {                    let name = s[4..s.len() - 1].trim_matches('`').to_string();                    let id = d.unique(slug(&name));                    o.push_str(&format!("<span id=\"{}\"></span>\n", attr(&id)));                }                i += 1;                let _ = indented(lines, &mut i, body_pad);                continue;            }            if let Some(r) = t.trim_start().strip_prefix(".. [") {                if r.contains(']') {                    i += 1;                    let _ = indented(lines, &mut i, body_pad);                    continue;                }            }            if let Some((name, arg)) = dir_parse(t) {                i += 1;                let inner = body_pad.min(lines.get(i).map_or(body_pad, |n| if n.trim().is_empty() { body_pad } else { indent_of(n).max(pad + 1) }));                let mut rest = indented(lines, &mut i, inner);                let mut opts = BTreeMap::new();                let mut k = 0;                while k < rest.len() {                    if let Some((a, b)) = field(&rest[k]) {                        opts.insert(a.to_lowercase(), b);                        k += 1;                    } else {                        break;                    }                }                rest.drain(..k);                if rest.first().map_or(false, String::is_empty) {                    rest.remove(0);                }                o.push_str(&directive(&name, &arg, &opts, &dedent(&rest), d, depth));                continue;            }            // A comment.            i += 1;            let _ = indented(lines, &mut i, pad + 1);            continue;        }        if t.trim_start().starts_with("__ ") {            i += 1;            continue;        }        // Grid table.        if t.trim_start().starts_with("+-") && t.ends_with('+') {            let start = i;            while i < lines.len() && !lines[i].trim().is_empty() {                i += 1;            }            let body: Vec<String> = lines[start..i].iter().map(|x| x.trim().to_string()).collect();            match grid_table(&body, d) {                Some(h) => o.push_str(&h),                None => {                    d.problems.push(format!("line {}: a grid table that does not line up", start + 1));                    o.push_str(&format!("<pre>{}</pre>\n", esc(&body.join("\n"))));                }            }            continue;        }        // Simple table.        if t.starts_with("==") && t.contains(' ') && t.chars().all(|c| c == '=' || c == ' ') {            let start = i;            let mut seen = 0;            while i < lines.len() {                if lines[i].trim_end() == t {                    seen += 1;                }                i += 1;                if seen >= 2 && lines.get(i).map_or(true, |x| x.trim().is_empty()) {                    // A third rule may follow a header's body.                    let more = lines[i..].iter().position(|x| x.trim_end() == t);                    let blank_gap = lines.get(i + 1).map_or(true, |x| x.trim().is_empty());                    if more.is_none() || seen >= 3 || blank_gap {                        break;                    }                }            }            let body: Vec<String> = lines[start..i.min(lines.len())].iter().map(|x| x.to_string()).collect();            match simple_table(&body, d) {                Some(h) => o.push_str(&h),                None => o.push_str(&format!("<pre>{}</pre>\n", esc(&body.join("\n")))),            }            continue;        }        // Lists.        if let Some(_first) = bullet(t) {            o.push_str("<ul>\n");            while i < lines.len() && indent_of(lines[i]) == pad && bullet(lines[i]).is_some() {                let text = bullet(lines[i]).unwrap_or("").to_string();                let at = lines[i].chars().count() - text.chars().count();                i += 1;                let mut body = vec![text];                body.extend(indented(lines, &mut i, at));                let h = sub_blocks(&body, d, depth);                o.push_str(&format!("<li>{}</li>\n", strip_p(&h)));                while i < lines.len() && lines[i].trim().is_empty() {                    i += 1;                }            }            o.push_str("</ul>\n");            continue;        }        if let Some((form, kind, start, text_at)) = enumerator(t) {            // A paragraph that happens to start `A. Name` is left alone unless the next line fits.            let continues = lines.get(i + 1).map_or(true, |n| n.trim().is_empty() || indent_of(n) >= text_at || enumerator(n).is_some());            if continues {                let tag_type = match kind { 'a' => " type=\"a\"", 'A' => " type=\"A\"", 'i' => " type=\"i\"", 'I' => " type=\"I\"", _ => "" };                let st = if start != 1 { format!(" start=\"{}\"", start) } else { String::new() };                o.push_str(&format!("<ol{}{}>\n", tag_type, st));                while i < lines.len() && indent_of(lines[i]) == pad {                    let Some((f2, k2, _, at)) = enumerator(lines[i]) else { break };                    if f2 != form || (k2 != kind && k2 != '#') {                        break;                    }                    let text: String = lines[i].chars().skip(at).collect();                    i += 1;                    let mut body = vec![text];                    body.extend(indented(lines, &mut i, at));                    let h = sub_blocks(&body, d, depth);                    o.push_str(&format!("<li>{}</li>\n", strip_p(&h)));                    while i < lines.len() && lines[i].trim().is_empty() {                        i += 1;                    }                }                o.push_str("</ol>\n");                continue;            }        }        if field(t).is_some() {            o.push_str("<dl class=\"fields\">\n");            while i < lines.len() {                let Some((k, v)) = field(lines[i]) else { break };                i += 1;                let mut body = vec![v];                let inner = lines.get(i).map_or(pad + 1, |n| if n.trim().is_empty() { pad + 1 } else { indent_of(n).max(pad + 1) });                body.extend(indented(lines, &mut i, inner));                let h = sub_blocks(&body, d, depth);                o.push_str(&format!("<dt>{}</dt>\n<dd>{}</dd>\n", esc(&k), strip_p(&h)));                while i < lines.len() && lines[i].trim().is_empty() {                    i += 1;                }            }            o.push_str("</dl>\n");            continue;        }        if t.trim_start() == "|" || t.trim_start().starts_with("| ") {            o.push_str("<div class=\"line-block\">\n");            while i < lines.len() {                let x = lines[i].trim_start();                if let Some(r) = x.strip_prefix("| ") {                    let mut text = r.to_string();                    i += 1;                    // A line starting with a space goes on from the last.                    while i < lines.len() && lines[i].starts_with(' ') && !lines[i].trim().is_empty() && !lines[i].trim_start().starts_with('|') {                        text.push(' ');                        text.push_str(lines[i].trim());                        i += 1;                    }                    let spaces = text.chars().take_while(|c| *c == ' ').count();                    o.push_str(&format!("<div class=\"line\">{}{}</div>\n", "\u{a0}".repeat(spaces), inline(text.trim_start(), d)));                } else if x.trim_end() == "|" {                    o.push_str("<div class=\"line\"></div>\n");                    i += 1;                } else {                    break;                }            }            o.push_str("</div>\n");            continue;        }        if t.trim_start().starts_with(">>> ") {            let mut b = vec![];            while i < lines.len() && !lines[i].trim().is_empty() {                b.push(lines[i].trim_start().to_string());                i += 1;            }            o.push_str(&format!("<pre class=\"doctest\">{}</pre>\n", esc(&b.join("\n"))));            continue;        }        if pad > 0 {            let body = indented(lines, &mut i, pad);            o.push_str(&format!("<blockquote>\n{}</blockquote>\n", sub_blocks(&body, d, depth)));            continue;        }        // A definition: a line with a deeper one under it.        if lines.get(i + 1).map_or(false, |n| !n.trim().is_empty() && indent_of(n) > pad) {            o.push_str("<dl>\n");            while i + 1 < lines.len() && !lines[i].trim().is_empty() && indent_of(lines[i]) == pad && indent_of(lines[i + 1]) > pad && !lines[i + 1].trim().is_empty() {                let (term, cls) = lines[i].trim().split_once(" : ").map_or((lines[i].trim(), ""), |x| x);                let term_html = inline(term, d);                let cls_html = if cls.is_empty() { String::new() } else { format!(" <span class=\"classifier\">{}</span>", inline(cls, d)) };                i += 1;                let inner = indent_of(lines[i]);                let body = indented(lines, &mut i, inner);                let h = sub_blocks(&body, d, depth);                o.push_str(&format!("<dt>{}{}</dt>\n<dd>{}</dd>\n", term_html, cls_html, strip_p(&h)));                while i < lines.len() && lines[i].trim().is_empty() {                    i += 1;                }            }            o.push_str("</dl>\n");            continue;        }        // A paragraph, and `::` for what comes after it.        let mut para = vec![t.trim_start().to_string()];        i += 1;        while i < lines.len() && !lines[i].trim().is_empty() && !(indent_of(lines[i]) == 0 && is_adornment(lines[i]) && lines[i].trim_end().len() >= 4) {            para.push(lines[i].trim().to_string());            i += 1;        }        let mut text = para.join(" ");        let literal = text.ends_with("::");        if literal {            if text == "::" {                text.clear();            } else if text.ends_with(" ::") {                text.truncate(text.len() - 3);            } else {                text.pop();            }        }        if !text.is_empty() {            o.push_str(&format!("<p>{}</p>\n", inline(&text, d)));        }        if literal {            while i < lines.len() && lines[i].trim().is_empty() {                i += 1;            }            if i < lines.len() && indent_of(lines[i]) > pad {                let inner = indent_of(lines[i]);                let body = indented(lines, &mut i, inner);                o.push_str(&format!("<pre class=\"literal\">{}</pre>\n", esc(&body.join("\n"))));            }        }    }    o}fn run(text: &str) -> (String, Doc) {    let text = text.trim_start_matches('\u{feff}').replace("\r\n", "\n").replace('\t', "        ");    let lines: Vec<&str> = text.split('\n').collect();    let mut d = Doc::default();    collect(&lines, &mut d);    // First pass over the sections, so a reference may name a later title.    let mut scan = Doc { targets: d.targets.clone(), anon: d.anon.clone(), subs: d.subs.clone(), foot_defs: d.foot_defs.clone(), manual: d.manual.clone(), ..Doc::default() };    let _ = blocks(&lines, &mut scan, 0);    for h in &scan.heads {        if let Some(t) = h["title"].as_str() {            d.targets.entry(norm(t)).or_default();        }    }    let mut body = blocks(&lines, &mut d, 0);    if d.toc_wanted {        let list: String = d.heads.iter().skip(1).map(|h| format!("<li class=\"level{}\"><a href=\"#{}\">{}</a></li>\n", h["level"], attr(h["id"].as_str().unwrap_or("")), esc(h["title"].as_str().unwrap_or("")))).collect();        while let Some(a) = body.find("\u{3}TOC") {            let e = a + body[a + 4..].find('\u{3}').unwrap_or(0) + 5;            let title = body[a + 4..e - 1].to_string();            body.replace_range(a..e + 1, &format!("<nav class=\"contents\">\n<p class=\"title\">{}</p>\n<ul>\n{}</ul>\n</nav>\n", title, list));        }    }    if !d.foot_refs.is_empty() {        body.push_str("<ol class=\"footnotes\">\n");        for k in d.foot_refs.clone() {            let t = d.foot_defs.iter().find(|f| f.0 == k).map(|f| f.1.clone()).unwrap_or_default();            let h = inline(&t, &mut d);            body.push_str(&format!("<li id=\"fn-{0}\"><span class=\"label\">[{0}]</span> {1}</li>\n", attr(&k), h));        }        body.push_str("</ol>\n");    }    (body, d)}/// reStructuredText as HTML. Raw blocks and includes are refused; `javascript:` links dropped; text is escaped.pub fn rst_to_html(text: &str) -> String {    run(text).0}/// The document as data: `{ title, headings, html, problems }`, with references that lead nowhere, short underlines, and unknown directives and roles among the problems.pub fn rst_parse(text: &str) -> Value {    let (html, d) = run(text);    json!({ "title": d.title, "headings": d.heads, "html": html, "problems": d.problems })}/// The field list at the top of a document, its bibliographic fields: `:Author: Ada` as `{ author: "Ada" }`.pub fn rst_fields(text: &str) -> Value {    let t = text.trim_start_matches('\u{feff}').replace("\r\n", "\n");    let lines: Vec<&str> = t.lines().collect();    let mut i = 0;    // Past a title, if there is one.    while i < lines.len() && (lines[i].trim().is_empty() || is_adornment(lines[i]) || (lines.get(i + 1).map_or(false, |n| is_adornment(n)))) {        i += 1;    }    let mut m = Map::new();    while i < lines.len() {        let Some((k, v)) = field(lines[i]) else { break };        i += 1;        let mut text = v;        while i < lines.len() && indent_of(lines[i]) > 0 && !lines[i].trim().is_empty() {            text.push(' ');            text.push_str(lines[i].trim());            i += 1;        }        m.insert(k.to_lowercase(), json!(text));    }    Value::Object(m)}
AsciiDoc asciidoc.qwer · 1.0.0AsciiDoc to HTML: headings with ids, attributes and conditionals, nested lists with continuation, description lists, tables with spans, admonitions, source blocks with callouts, quotes, images, footnotes, a table of contents. Text is escaped and `javascript:` links dropped, raw passthrough refused unless asked for, and includes refused or kept inside one folder, with line ranges and tags.markupdocumentationhtmldocs6 views · 11 downloadsHTML to Markdown html_markdown.qwer · 1.0.0HTML turned into clean Markdown as Turndown does: headings, lists, links, images, code blocks, quotes and tables kept, scripts and the parts you name dropped; for importing old posts, editor output, and pages for an LLM.htmlmarkdownimportllm7 views · 6 downloadsHTML to text html_text.qwer · 1.0.0HTML as readable plain text for the text part of an email: headings, lists and tables laid out, links listed at the end, and one-line previews.emailhtmltextexport7 views · 7 downloadsLaTeX maths latex_math.qwer · 1.0.0LaTeX maths to MathML through pulldown-latex: inline formulas, displayed blocks and whole texts with $...$ in them, drawn by browsers with no script, and mistakes placed by line and column.mathmathmllatexmarkup4 views · 8 downloadsOrg files org_mode.qwer · 1.0.0Org files read with no Emacs: nested headlines with TODO keywords from the file's own #+TODO, priorities, tags inherited, properties, planning lines, timestamps with ranges, repeaters and warnings, tables as records. HTML for the lot, and an agenda for a date range with repeaters expanded and done items left out.org-modenotesagendahtml3 views · 8 downloadsReadability readability.qwer · 1.0.0A web page's article without its menus, ads and footer: title, author, date, clean HTML, plain text or Markdown, for read-later apps, link previews and summaries.htmlscrapingtextmarkdownimport8 views · 7 downloads
Thread
36Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.