Skip to content

Commit 819f999

Browse files
committed
Added markdowntoc.pl, and an entry for it in README
1 parent f20a778 commit 819f999

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
# pubtools
2+
23
Utilities, tools, and code that I have written or acquired, that I find useful to do miscelaneous tasks. Please keep attribution and copyright with anything you use/distribute.
4+
5+
# Files
6+
7+
* markdowntoc.pl: Configurable script to generate a table of contents for a github markdown document.

markdowntoc.pl

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/usr/bin/perl
2+
#
3+
# markdowntoc V1.0
4+
#
5+
# Makes headings in a markdown document into a table of contents at the top (or, optionally, somewhere else).
6+
# Intended for use with Github wikis.
7+
# Before use, make adjustments in the CONFIGURATION section.
8+
# See also the THINGS TO KNOW section.
9+
#
10+
# Copyright (C) 2019 Luke Davis <[email protected]>, all rights reserved.
11+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
12+
# You may obtain a copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>
13+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and limitations under the License.
16+
#
17+
# This script has been tested with a very specific set of github wikis. It has not been widely tested at all.
18+
# In other words: Make backups!
19+
# This script took initial inspiration from an article written by Grant Winney, at <https://grantwinney.com/5-things-you-can-do-with-a-locally-cloned-github-wiki/>.
20+
#
21+
# Changelog:
22+
# V1.0: initial stable release (6/5/2019)
23+
24+
### THINGS TO KNOW ###
25+
26+
# Usage:
27+
# markdowntoc.pl file1.md [ file2.md ... fileN.md ]
28+
# The current version takes one or more filenames on the command line.
29+
# Each file has a temporary version written in the working directory with the ".mdw" extension added.
30+
# After successful completion, the temporary file is copied over the source file and deleted.
31+
# No file locking is performed! This script is very fast, so that isn't usually an issue, but be aware.
32+
#
33+
# You may place links to the TOC anywhere in your markdown, by including the following text: [Table Of Contents]
34+
# The text is configured by the "tocBegin" variable, set in CONFIGURATION below.
35+
#
36+
# By default, the TOC is generated at the very top of your file(s).
37+
# Since this is probably not what you want--normally a TOC should appear under some introductory text--you can
38+
# adjust the placement of the TOC by putting the following line where you want the TOC to appear:
39+
# [//]: # (Place this line where you want the table of contents to start)
40+
# (Not including the initial hashmark and space, of course).
41+
# To save you from having to enter this, run the script without it the first time. It will insert the line at the top.
42+
# Move the line using your editor to wherever you want it, and run the script again. It will move the table of contents to that spot.
43+
44+
### CONFIGURATION ###
45+
46+
# If you want to collapse one level of headings, set this to 1 (or higher).
47+
# For example, if your document starts with an H1, and has everything else at H2, and you would rather pretend those H2s are H1s
48+
# for HTML list nesting purposes, set this to 1.
49+
my $collapseLevels = 1; # Default should be 0 for no collapsing
50+
51+
# If you don't want to include H1 headings in your TOC, set this to 2. If you don't want to include H1 or H2, set this to 3.
52+
my $startWithLevel = 2; # Default should be 1 to include all heading levels starting at H1
53+
54+
# The markdown header placed at the start of your TOC--this is intended to be visible to users.
55+
my $tocHeader = "\n# **TABLE OF CONTENTS**\n\n";
56+
57+
# The script will search for this line when deciding where to place the TOC.
58+
# If it finds it, it will place the TOC after it; otherwise it will place it at the very top and insert this line above it.
59+
# Don't change this unless you really understand the markdown syntax!
60+
my $tocPlaceholder = "[//]: # (Place this line where you want the table of contents to start)\n";
61+
62+
# These are comments used by the script to find a previously generated TOC.
63+
# They are hidden from people viewing the parsed version of the markdown.
64+
# Don't change these unless you really understand the markdown syntax!
65+
my $tocBegin = "[Table Of Contents]: <#user-content-table-of-contents> (TOC)\n";
66+
my $tocEnd = "[//]: # (End of TOC)\n";
67+
68+
### END OF CONFIGURATION ###
69+
70+
use strict;
71+
use warnings;
72+
use File::Copy;
73+
74+
# Initializations and sanity checks
75+
my @headers = ();
76+
my $foundTOCPlaceHolder = 0;
77+
78+
if ($startWithLevel <= 0) {
79+
$startWithLevel = 1;
80+
}
81+
82+
if ($collapseLevels < 0) {
83+
$collapseLevels = 0;
84+
}
85+
86+
# Process command line files
87+
foreach my $file (<*.md>) {
88+
# First pass
89+
open(my $fh, '<', $file) or die "Can't open $file: $!";
90+
my @lines = <$fh>;
91+
close $fh or die "Can't close $file: $!";
92+
93+
@headers = ();
94+
95+
foreach (@lines) {
96+
my $headerLevel = 0;
97+
if ($foundTOCPlaceHolder == 0 and $_ eq $tocPlaceholder) {
98+
$foundTOCPlaceHolder = 1;
99+
}
100+
if ($_ =~ /^######/) {
101+
$headerLevel = 6;
102+
}
103+
elsif ($_ =~ /^#####/) {
104+
$headerLevel = 5;
105+
}
106+
elsif ($_ =~ /^####/) {
107+
$headerLevel = 4;
108+
}
109+
elsif ($_ =~ /^###/) {
110+
$headerLevel = 3;
111+
}
112+
elsif ($_ =~ /^##/) {
113+
$headerLevel = 2;
114+
}
115+
elsif ($_ =~ /^#/) {
116+
$headerLevel = 1;
117+
}
118+
if ($headerLevel > 0) { # It's a heading, not a normal line
119+
if ($headerLevel >= $startWithLevel) { # Only include headers at or above the start with level
120+
push @headers, createLink($_, $headerLevel, $collapseLevels);
121+
}
122+
}
123+
}
124+
125+
if (scalar(@headers) == 0) {
126+
next;
127+
}
128+
129+
# Second pass
130+
open(my $in, '<', $file) or die "Can't open $file' $!";
131+
open(my $out, '>', "$file.mdw") or die "Can't write $file.mdw: $!";
132+
133+
my $traversingOldToc = 0;
134+
my $printedTOC = 0;
135+
while(<$in>) {
136+
# These comparisons all have to be done in a very specific order.
137+
# If we're already in an old TOC, all we want to do is eliminate it
138+
if ($traversingOldToc == 1) {
139+
# If this line is the last of an old TOC, we stop eliminating after this line
140+
if ($_ eq $tocEnd) {
141+
$traversingOldToc = 0;
142+
}
143+
next; # Don't feed this line through to the output
144+
}
145+
# If a TOC placeholder was found in first pass, check for it and insert the TOC there when found
146+
# If not, then we insert the TOC at the top
147+
if ($printedTOC == 0) {
148+
if ($foundTOCPlaceHolder == 1) {
149+
if ($_ eq $tocPlaceholder) { # Found it!
150+
print $out $_;
151+
print $out generateTOC();
152+
$printedTOC = 1; # Make sure we don't print it again
153+
next; # We already printed this line; process the next one
154+
}
155+
} else { # Didn't find a placeholder; print the TOC at top of output
156+
print $out $tocPlaceholder;
157+
print $out generateTOC();
158+
$printedTOC = 1;
159+
}
160+
}
161+
# If this line starts an old TOC, we get rid of it and flag further lines as old TOC lines
162+
if ($_ eq $tocBegin) {
163+
$traversingOldToc = 1;
164+
next; # Don't feed this line through to the output
165+
}
166+
print $out $_; # If the line hasn't been processed in some way, send it through to the output
167+
}
168+
169+
close $in or die "Can't close $file: $!";
170+
close $out or die "Can't close $file.mdw: $!";
171+
172+
move("$file.mdw", $file) or die "Can't rename $file.mdw to $file: $!";
173+
}
174+
175+
# Returns a string containing a single HTML list item for the TOC
176+
# @params:
177+
# String containing a markdown header line
178+
# Int indicating number of indentation (H1 is 1, H2 is 2, etc.)
179+
# Int indicating how many levels of indentation to collapse (see the $collapseLevels CONFIGURATION variable)
180+
sub createLink {
181+
my $currentLine = $_[0];
182+
my $indent = $_[1];
183+
my $collapseLevels = $_[2];
184+
185+
my $text = substr($currentLine, $indent);
186+
187+
$text =~ s/^\s+|\s+$//g; # Strip spaces from either end
188+
189+
my $link = lc $text =~ s/[,\[\]\;:\/\?\"\'\*\+\.]+//gr; # Remove undesirable punctuation
190+
$link =~ s/ /-/g;
191+
192+
if ($collapseLevels > 0) {
193+
$indent = $indent - $collapseLevels;
194+
}
195+
if ($indent < 0) { # In case the above went too far
196+
$indent = 0;
197+
}
198+
199+
return " " x (($indent-1)*2) . "- " . "<a href=\"#user-content-$link\">$text</a>\n";
200+
}
201+
202+
# Returns a string containing the HTML TOC
203+
sub generateTOC {
204+
my $output = $tocBegin . $tocHeader;
205+
foreach(@headers) {
206+
$output = $output . $_;
207+
}
208+
209+
$output = $output . "\n---\n\n";
210+
$output = $output . $tocEnd;
211+
return $output;
212+
}

0 commit comments

Comments
 (0)