Program.cs
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
namespace toc
{
public class Builder
{
public static string getAnchorId(string title)
{
StringBuilder sb = new StringBuilder();
title = title.Trim().ToLower();
foreach (var ch in title)
{
if (ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9'
|| ch == '-')
sb.Append(ch);
else if (ch==' ' || ch =='_')
sb.Append('-');
}
return sb.ToString();
}
public class Node
{
public List<string> fileNamePrefix = new List<string>();
public string title;
public string shortTitle;
public string fileID;
public List<string> sections = new List<string>();
public List<string> sectionShortTitles = new List<string>();
public List<Node> children = new List<Node>();
}
public static void buildTOC(StringBuilder sb, Node n)
{
sb.AppendFormat("<li data-link=\"{0}\"><span>{1}</span>\n", n.fileID, n.shortTitle);
if (n.children.Count != 0)
{
sb.AppendLine("<ul class=\"toc_list\">");
foreach(var c in n.children)
buildTOC(sb, c);
sb.AppendLine("</ul>");
}
else if (n.sections.Count != 0)
{
sb.AppendLine("<ul class=\"toc_list\">");
for (int i = 0; i < n.sections.Count; i++)
{
var s = n.sections[i];
var shortTitle = n.sectionShortTitles[i];
sb.AppendFormat("<li data-link=\"{0}#{1}\"><span>{2}</span></li>\n", n.fileID, getAnchorId(s), shortTitle);
}
sb.AppendLine("</ul>");
}
sb.AppendLine("</li>");
}
public static string buildTOC(Node n)
{
StringBuilder sb = new StringBuilder();
sb.Append(@"<ul class=""toc_root_list"">");
buildTOC(sb, n);
sb.Append(@"</ul>");
return sb.ToString();
}
public static bool isChild(Node parent, Node child)
{
if (parent.fileNamePrefix.Count < child.fileNamePrefix.Count)
{
bool equal = true;
for (int k = 0; k < parent.fileNamePrefix.Count; k++)
{
if (parent.fileNamePrefix[k] != child.fileNamePrefix[k])
{
equal = false;
break;
}
}
return equal;
}
return false;
}
public static string getNextNonEmptyLine(string[] lines, int i)
{
i++;
while (i < lines.Length)
{
if (lines[i].Trim().Length != 0)
return lines[i];
i++;
}
return "";
}
const string shortTitlePrefix = "[//]: # (ShortTitle: ";
public static string maybeGetShortTitleImpl(string originalTitle, string[] lines, int line)
{
string nextLine = getNextNonEmptyLine(lines, line);
if (nextLine.StartsWith(shortTitlePrefix))
{
return nextLine.Substring(shortTitlePrefix.Length, nextLine.Length - shortTitlePrefix.Length - 1).Trim();
}
return originalTitle;
}
public static string escapeString(string input)
{
StringBuilder sb = new StringBuilder();
foreach (var ch in input)
{
if (ch == '<')
sb.Append("<");
else if (ch == '>')
sb.Append(">");
else
sb.Append(ch);
}
return sb.ToString();
}
public static string maybeGetShortTitle(string originalTitle, string[] lines, int line)
{
string title = maybeGetShortTitleImpl(originalTitle, lines, line);
return escapeString(title);
}
public static string Run(string path)
{
StringBuilder outputSB = new StringBuilder();
outputSB.AppendFormat("Building table of contents from {0}...\n", path);
var files = System.IO.Directory.EnumerateFiles(path, "*.md");
List<Node> nodes = new List<Node>();
foreach (var f in files)
{
var content = File.ReadAllLines(f);
Node node = new Node();
node.fileID = Path.GetFileNameWithoutExtension(f);
outputSB.AppendFormat(" {0}.md\n", node.fileID);
bool mainTitleFound = false;
for (int i = 1; i < content.Length; i++)
{
if (content[i].StartsWith("==="))
{
mainTitleFound = true;
node.title = content[i-1];
node.shortTitle = maybeGetShortTitle(node.title, content, i);
}
if (content[i].StartsWith("---"))
{
if (!mainTitleFound) continue;
node.sections.Add(content[i-1]);
node.sectionShortTitles.Add(maybeGetShortTitle(content[i - 1], content, i));
}
if (content[i].StartsWith("#") && !content[i].StartsWith("##") && node.title == null)
{
mainTitleFound = true;
node.title = content[i].Substring(1, content[i].Length - 1).Trim();
node.shortTitle = maybeGetShortTitle(node.title, content, i);
}
if (content[i].StartsWith("##") && !content[i].StartsWith("###"))
{
if (!mainTitleFound) continue;
var sectionStr = content[i].Substring(2, content[i].Length - 2).Trim();
node.sections.Add(sectionStr);
node.sectionShortTitles.Add(maybeGetShortTitle(sectionStr, content, i));
}
}
if (node.title == null)
{
outputSB.AppendFormat("Error: {0} does not define a title.", f);
node.title = "Untitiled";
}
var titleSecs = Path.GetFileName(f).Split('-');
foreach (var s in titleSecs)
{
if (s.Length == 2 && s[1]>='0' && s[1] <= '9')
{
node.fileNamePrefix.Add(s);
}
else
{
break;
}
}
// Find parent node.
Node parent=null;
for (int l = nodes.Count-1; l>=0; l--)
{
var n = nodes[l];
if (isChild(n, node))
{
parent = n;
break;
}
}
if (parent != null)
parent.children.Add(node);
else
{
// find child
foreach (var other in nodes)
{
if (isChild(node, other))
{
node.children.Add(other);
}
}
foreach (var c in node.children)
{
nodes.Remove(c);
}
nodes.Add(node);
}
}
var root = nodes.Find(x=>x.fileID=="index");
if (root != null)
{
var html = buildTOC(root);
var outPath = Path.Combine(path, "toc.html");
File.WriteAllText(outPath, html);
outputSB.AppendFormat("Output written to: {0}\n", outPath);
}
return outputSB.ToString();
}
}
}