feat(html): html_header_numbering section numbering for HTML export
Mirror vimwiki's html_header_numbering / html_header_numbering_sym: top-level headings at or below the start level get a dotted section number (1, 1.1, 1.2, ...) with a configurable trailing symbol. Off by default (level 0). Nested headings in lists/quotes stay unnumbered, matching upstream's document-level-only scan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,14 @@ pub struct HtmlRenderer {
|
||||
/// outermost list. `-1` (the default) emits no inline style and
|
||||
/// defers to the stylesheet; `>= 0` forces `margin-left:<n>em`.
|
||||
list_margin: i32,
|
||||
/// vimwiki `html_header_numbering`: the heading level at which automatic
|
||||
/// section numbering begins (`0` = off, the default). When `>= 1`, every
|
||||
/// heading at that level or deeper is prefixed with a dotted section
|
||||
/// number (`1`, `1.1`, `1.2`, …); shallower headings stay unnumbered.
|
||||
header_numbering: u8,
|
||||
/// vimwiki `html_header_numbering_sym`: a symbol appended after the
|
||||
/// section number (e.g. `.` → `1.2. Heading`). Empty by default.
|
||||
header_numbering_sym: String,
|
||||
}
|
||||
|
||||
impl Default for HtmlRenderer {
|
||||
@@ -66,6 +74,8 @@ impl HtmlRenderer {
|
||||
vars: HashMap::new(),
|
||||
colors: HashMap::new(),
|
||||
list_margin: -1,
|
||||
header_numbering: 0,
|
||||
header_numbering_sym: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +124,61 @@ impl HtmlRenderer {
|
||||
self.colors = colors;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable vimwiki-style HTML section numbering. `level` is the heading
|
||||
/// level at which numbering starts (`0` disables it); `sym` is appended
|
||||
/// after the number. Mirrors `html_header_numbering` /
|
||||
/// `html_header_numbering_sym`.
|
||||
pub fn with_header_numbering(mut self, level: u8, sym: impl Into<String>) -> Self {
|
||||
self.header_numbering = level;
|
||||
self.header_numbering_sym = sym.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Running state for vimwiki-style HTML section numbering
|
||||
/// (`html_header_numbering`). Headings are visited in document order; each
|
||||
/// at or below the start level bumps its depth's counter, resets deeper
|
||||
/// counters, and yields a dotted prefix (`1`, `1.1`, `2`, …).
|
||||
struct HeadingNumberer {
|
||||
/// Heading level numbering starts at (`0` disables it).
|
||||
start: u8,
|
||||
/// Symbol appended after the dotted number.
|
||||
sym: String,
|
||||
/// Per-depth counters, indexed by `level - start` (levels clamp to 1–6,
|
||||
/// so the depth is always `0..=5`).
|
||||
counters: [u32; 6],
|
||||
}
|
||||
|
||||
impl HeadingNumberer {
|
||||
fn new(start: u8, sym: &str) -> Self {
|
||||
Self {
|
||||
start,
|
||||
sym: sym.to_string(),
|
||||
counters: [0; 6],
|
||||
}
|
||||
}
|
||||
|
||||
/// Advance to a heading at `level` (1–6) and return its numeric prefix,
|
||||
/// including the trailing symbol and a separating space (e.g. `"1.2. "`).
|
||||
/// Returns an empty string when numbering is off or the heading is
|
||||
/// shallower than the start level.
|
||||
fn prefix(&mut self, level: u8) -> String {
|
||||
if self.start == 0 || level < self.start {
|
||||
return String::new();
|
||||
}
|
||||
let depth = (level - self.start) as usize;
|
||||
self.counters[depth] += 1;
|
||||
for c in self.counters.iter_mut().skip(depth + 1) {
|
||||
*c = 0;
|
||||
}
|
||||
let num = self.counters[..=depth]
|
||||
.iter()
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(".");
|
||||
format!("{num}{} ", self.sym)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for HtmlRenderer {
|
||||
@@ -146,15 +211,25 @@ impl Renderer for HtmlRenderer {
|
||||
|
||||
impl HtmlRenderer {
|
||||
fn render_body(&self, doc: &DocumentNode, w: &mut dyn Write) -> io::Result<()> {
|
||||
// Section numbering runs over top-level headings in document order,
|
||||
// mirroring vimwiki's line-by-line scan. Nested headings (in lists,
|
||||
// quotes) go through `render_block` and are left unnumbered, matching
|
||||
// upstream which only numbers document-level headers.
|
||||
let mut numberer = HeadingNumberer::new(self.header_numbering, &self.header_numbering_sym);
|
||||
for block in &doc.children {
|
||||
self.render_block(block, w)?;
|
||||
if let BlockNode::Heading(n) = block {
|
||||
let number = numberer.prefix(n.level.clamp(1, 6));
|
||||
self.render_heading(n, &number, w)?;
|
||||
} else {
|
||||
self.render_block(block, w)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_block(&self, block: &BlockNode, w: &mut dyn Write) -> io::Result<()> {
|
||||
match block {
|
||||
BlockNode::Heading(n) => self.render_heading(n, w),
|
||||
BlockNode::Heading(n) => self.render_heading(n, "", w),
|
||||
BlockNode::Paragraph(n) => self.render_paragraph(n, w),
|
||||
BlockNode::HorizontalRule(n) => self.render_hr(n, w),
|
||||
BlockNode::Blockquote(n) => self.render_blockquote(n, w),
|
||||
@@ -185,7 +260,10 @@ impl HtmlRenderer {
|
||||
w.write_all(b"</div>\n")
|
||||
}
|
||||
|
||||
fn render_heading(&self, n: &HeadingNode, w: &mut dyn Write) -> io::Result<()> {
|
||||
/// Render a heading. `number` is the optional section-number prefix
|
||||
/// (already including its trailing symbol and space, e.g. `"1.2. "`);
|
||||
/// pass `""` for no numbering.
|
||||
fn render_heading(&self, n: &HeadingNode, number: &str, w: &mut dyn Write) -> io::Result<()> {
|
||||
let level = n.level.clamp(1, 6);
|
||||
let class = if n.centered {
|
||||
" class=\"centered\""
|
||||
@@ -193,6 +271,9 @@ impl HtmlRenderer {
|
||||
""
|
||||
};
|
||||
write!(w, "<h{level}{class}>")?;
|
||||
if !number.is_empty() {
|
||||
write_escaped(number, w)?;
|
||||
}
|
||||
self.render_inlines(&n.children, w)?;
|
||||
writeln!(w, "</h{level}>")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user