Brush

toc.tcl
Login

File doc/toc.tcl from the latest check-in


#!/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: