Skip to content

Instantly share code, notes, and snippets.

@jakejscott
Last active March 16, 2016 22:33
Show Gist options
  • Save jakejscott/dfd22a747919c4d7042e to your computer and use it in GitHub Desktop.
Save jakejscott/dfd22a747919c4d7042e to your computer and use it in GitHub Desktop.
Add a table of contents to a markdown document
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CommonMark;
using CommonMark.Formatters;
using CommonMark.Syntax;
namespace ConsoleApp
{
public class Program
{
// blocks are elemets like paragraphs and lists, inlines are
// elements like emphasis, links, images.
public static void Main(string[] args)
{
CommonMarkSettings.Default.OutputDelegate = (document, textWriter, settings) =>
{
var formatter = new CustomHtmlFormatter(document, textWriter, settings);
formatter.WriteDocument(document);
};
using (var reader = new StreamReader("./README.md"))
{
var document = CommonMarkConverter.Parse(reader);
using (var writer = new StringWriter())
{
// write the HTML output
CommonMarkConverter.ProcessStage3(document, writer);
Console.WriteLine(writer.ToString());
}
}
Console.ReadLine();
}
private class CustomHtmlFormatter : HtmlFormatter
{
private readonly List<EnumeratorEntry> headings;
public CustomHtmlFormatter(Block block, TextWriter target, CommonMarkSettings settings)
: base(target, settings)
{
headings = block.AsEnumerable().Where(
x => x.IsOpening &&
x.Block != null &&
x.Block.Tag == BlockTag.AtxHeading &&
x.Block.InlineContent != null
).ToList();
}
protected override void WriteBlock(Block block, bool isOpening, bool isClosing, out bool ignoreChildNodes)
{
if (block.Tag == BlockTag.AtxHeading && block.InlineContent != null)
{
//
// Add anchorable id to each heading
//
ignoreChildNodes = false;
if (isOpening)
{
var slug = Slug.Create(true, block.InlineContent.LiteralContent);
Write(string.Format("<h{0} id=\"{1}\">", block.Heading.Level, slug));
}
if (isClosing)
{
Write(string.Format("</h{0}>", block.Heading.Level));
}
}
else if (block.Tag == BlockTag.Paragraph && block.InlineContent != null && block.InlineContent.LiteralContent == "{:toc}" && headings.Any())
{
//
// Injects a table of contents.
//
ignoreChildNodes = true;
if (isOpening)
{
WriteLine();
Write("<ul>");
foreach (var heading in headings)
{
var text = heading.Block.InlineContent.LiteralContent;
var slug = Slug.Create(true, text);
WriteLine();
Write(string.Format("<li href=\"#{0}\">", slug));
Write(text);
Write("</li>");
}
}
if (isClosing)
{
WriteLine();
Write("</ul>");
WriteLine();
}
}
else
{
base.WriteBlock(block, isOpening, isClosing, out ignoreChildNodes);
}
}
}
}
}
using System;
using System.Text;
namespace ConsoleApp
{
public static class Slug
{
public static string Create(bool toLower, params string[] values)
{
return Create(toLower, String.Join("-", values));
}
public static string Create(bool toLower, string value)
{
if (value == null) return "";
var normalised = value.Normalize(NormalizationForm.FormKD);
const int maxlen = 80;
int len = normalised.Length;
bool prevDash = false;
var sb = new StringBuilder(len);
char c;
for (int i = 0; i < len; i++)
{
c = normalised[i];
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
{
if (prevDash)
{
sb.Append('-');
prevDash = false;
}
sb.Append(c);
}
else if (c >= 'A' && c <= 'Z')
{
if (prevDash)
{
sb.Append('-');
prevDash = false;
}
// tricky way to convert to lowercase
if (toLower)
sb.Append((char)(c | 32));
else
sb.Append(c);
}
else if (c == ' ' || c == ',' || c == '.' || c == '/' || c == '\\' || c == '-' || c == '_' || c == '=')
{
if (!prevDash && sb.Length > 0)
{
prevDash = true;
}
}
else
{
string swap = ConvertEdgeCases(c, toLower);
if (swap != null)
{
if (prevDash)
{
sb.Append('-');
prevDash = false;
}
sb.Append(swap);
}
}
if (sb.Length == maxlen) break;
}
return sb.ToString();
}
static string ConvertEdgeCases(char c, bool toLower)
{
string swap = null;
switch (c)
{
case 'ı':
swap = "i";
break;
case 'ł':
swap = "l";
break;
case 'Ł':
swap = toLower ? "l" : "L";
break;
case 'đ':
swap = "d";
break;
case 'ß':
swap = "ss";
break;
case 'ø':
swap = "o";
break;
case 'Þ':
swap = "th";
break;
}
return swap;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment