#!/usr/bin/env tclsh
package require Tcl 8.6
# Link back to table of contents.
set top {<a href="#table_of_contents" style="font-size: small">[top]</a>}
# Process each named file.
foreach file $argv {
# Read Markdown file.
set chan [open $file]
set data [chan read $chan]
chan close $chan
# Skip the file if it does not contain a TOC.
if {[string first <!--TOC--> $data] < 0} {
continue
}
# Identify all code blocks fenced by "```" lines. Permit the opening code
# fence to be followed by other characters, e.g. syntax mode name.
set fences [regexp -indices -line -inline -all {^```(?:.|\n)*?^```$} $data]
# Build the TOC to contain all first- and second-level headings. Consider
# only headings using "#" and "##" marks, not underlines, and skip headings
# that appear inside fenced code blocks.
set start 0
set offset 0
set toc <!--TOC-->
append toc "\n<a name=\"table_of_contents\"></a>"
append toc " Table of Contents\n" [string repeat = 50]
while {[regexp -indices -line -start $start {^##?[^#].*} $data match]} {
# Place the start and end indices in their own variables.
lassign $match match0 match1
# Skip this match if it starts within a fenced code block.
set skip 0
foreach fence $fences {
if {$match0 >= [lindex $fence 0] + $offset
&& $match0 <= [lindex $fence 1] + $offset} {
set skip 1
break
}
}
if {$skip} {
set start [expr {$match1 + 1}]
continue
}
# Get line from input.
set line [string range $data $match0 $match1]
# Get heading level.
regexp {^(#*)(.*)} $line _ heading line
# Strip the anchor tag if present.
regsub {^ <a name=".*"></a>} $line {} line
# Strip the link to the TOC if present.
regsub { <a href="#.*".*} $line {} line
# Extract the title.
set title [string trim $line]
# Compute the anchor name and markup.
regsub -all {\W} [string tolower $title] _ name
set anchor "<a name=\"$name\"></a>"
# Build table of contents.
append toc \n
if {$heading eq "##"} {
append toc " "
}
append toc "- \[$title\](#$name)"
# Replace the section header line.
set line "$heading $anchor $title $top"
set data [string replace $data $match0 $match1 $line]
# Update the start index and offset.
set start [expr {[string length $line] + $match0}]
incr offset [expr {$start - $match1 - 1}]
}
append toc \n
# Write Markdown file with the table of contents inserted.
set chan [open $file w]
chan puts -nonewline $chan [regsub -line {^<!--TOC-->$(?:\n.+$)*\n$}\
$data [string map {& \& \\ \\\\} $toc]]
chan close $chan
}
# vim: set sts=4 sw=4 tw=80 et ft=tcl: