Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 107 lines (85 sloc) 2.95 KB
#!/usr/bin/env ruby
# from Repo:
require 'optparse'
options = {
max_header_depth: 4, # the original is 3
toc_regex_string: '^%TOC(:([\d]+))?%$',
stdout: false,
dry_run: false,
} do |opts|
opts.banner = "Usage: #{__FILE__} FILE [options]"
opts.on( '-d', '--max-header-depth INT', Integer,
"Set the default max header depth (default: #{options[:max_header_depth]})") do |d|
options[:max_header_depth] = d
opts.on( '-r', '--toc-regex STRING', String,
"TOC regex to be matched (default: '#{options[:toc_regex_string]}'')") do |s|
options[:toc_regex_string] = s
opts.on('-s', '--stdout',
"Print the generated TOC to stdout") do
options[:stdout] = true
opts.on('-x', '--dry-run', 'Don\'t update file with TOC') do
options[:dry_run] = true
opts.on('-h', '--help', 'Print this help') do
puts opts
if ARGV.empty?
STDERR.puts 'ERROR: No file provided'
STDERR.puts opts
exit 1
toc_regex = options[:toc_regex_string]
# get the markdown input
file = ARGV[0]
input_text =
# find the headers in the markdown input
toc_found = false
headers = []
input_text.lines.each do |text|
# only link to headers below the TOC
toc_found = text =~ toc_regex unless toc_found
# TODO: update the max header depth based on the %TOC:\d% match
next unless toc_found
match = /^(?<depth>\#{1,#{options[:max_header_depth]}})[[:space:]]+(?<header_text>.*)/.match(text)
next unless match
headers << {depth: match['depth'].length, header_text: match['header_text']}
# error if toc not matched in file
raise "TOC regex #{toc_regex.inspect} not matched in #{file}" unless toc_found
# generate the toc markdown
toc_md = ''
anchors = []
headers.each_with_index do |h,i|
# make the anchor more anchor-looking
anchor = h[:header_text].gsub(/[^a-zA-Z0-9[[:space:]]-]/, '').gsub(/[[:space:]]+/, '-').downcase
# backslashes are the worst, they cannot be the last character of a GFM link text
h[:header_text].gsub!(/\\$/, '&#92;')
# indent the bullet based on the header depth (depth 1: '', depth 2: ' ', depth 3: ' ')
indent = (' ' * h[:depth] * 2)[0...-2]
# append a number if the same header text has already been used
count = 0
anchors.each { |a| count += 1 if a == anchor }
anchors << anchor
anchor << "-#{count}" if count > 0
# append the bullet to the toc text
toc_md << "#{indent}- [#{h[:header_text]}](##{anchor})"
# append newline for next bullet (unless last bullet)
toc_md << "\n" unless i == headers.length - 1
# update the markdown with the toc
output_text = input_text.gsub(toc_regex, toc_md)
if options[:stdout]
STDERR.puts "Generated TOC for #{file}:"
puts toc_md
unless options[:dry_run], 'w').write output_text
STDERR.puts "Updated TOC in #{file}"