An Extensible Markdown Class in C#
Tuesday, 21 April 2009
The Markdown Format is widely used on the web to enable users to enter basic HTML formatting without the need to verify full mark-up. Various C# implementations of the standard exist online, however for I only needed to implement some of the specification, whilst adding bespoke formatting options.
The implementations of the standard I found online had a couple of problems with them: firstly, there were no unit tests against them so I couldn’t verify their correct operation. Secondly, in order to extend them, I’d have to crack open the implementation and start messing around with the code.
Using the existing code as a base, I decided to re-write an implementation with a specification (unit tests) and points for future extensibility. I wanted to enable the resulting code to be closed to modification of its existing behaviour, but open to extension without requiring the need to dig into the existing codebase (the Open-Closed principle).
I decided to implement this using the visitor pattern. This defines a series of individual visitors, or marklets, which will each pass over the markdown input to transform it into mark-up. Because there is a chain of visitors, it is easy to append new visitors to the chain and extend the functionality of the class.
namespace Cogworks
{
/// <summary>
/// Converts Markdown into Markup
/// </summary>
static public class Markdown
{
private static readonly IList<abstractmarklet> Marklets;
/// <summary>
/// Initializes the <see cref="Markdown"/> class.
/// </summary>
static Markdown()
{
// You could initilize this from an IoC container if you wanted.
Marklets = new List<abstractmarklet>
{
new CleanupMarklet(),
new PreMarklet(),
new ItalicMarklet(),
new BoldMarklet(),
new NewParagraphMarklet(),
new BreakMarklet()
};
}
/// <summary>
/// Converts the given <see cref="markdown"/>
/// </summary>
/// <param name="markdown">The markdown.</param>
/// <returns></returns>
public static string ToMarkup(this String markdown)
{
foreach(var marklet in Marklets)
{
markdown = marklet.Markup(markdown);
}
return markdown;
}
}
}
As each marklet is a unique class, it is smaller and easier to understand, test and debug.
namespace Cogworks
{
/// <summary>
/// Converts bold Markdown into HTML markup.
/// </summary>
/// <example>
/// ##bold text##
/// </example>
public class BoldMarklet : AbstractMarklet
{
/// <summary>
/// Converts the given markdown into markup.
/// </summary>
/// <param name="value">The markdown input.</param>
/// <returns>HTML markup</returns>
public override string Markup(string value)
{
var markdown = string.Empty;
if (!string.IsNullOrEmpty(value))
{
markdown = Replace(value, @"\#\#(.*?)\#\#", Evaluator);
}
return markdown;
}
private static string Evaluator(Match match)
{
return string.Format("<strong>{0}</strong>", match.Groups[1].Value);
}
}
}
The unit tests represent the specification of the markdown language:
[TestFixture]
public class BoldMarkletTest
{
private BoldMarklet marklet;
[SetUp]
public void SetUp()
{
marklet = new BoldMarklet();
}
[Test]
public void TestMarkdownWhenNull()
{
var markup = marklet.Markup(null);
Assert.AreEqual(string.Empty, markup);
}
[Test]
public void TestMarkdownPlainText()
{
var markup = marklet.Markup("Plain Text");
Assert.AreEqual("Plain Text", markup);
}
[Test]
public void TestMarkdownBoldText()
{
var markup = marklet.Markup("Some ##Bold## Text");
Assert.AreEqual("Some <strong>Bold</strong> Text", markup);
}
[Test]
public void TestMarkdownUnclosedBoldText()
{
var markup = marklet.Markup("Some ##Bold Text");
Assert.AreEqual("Some ##Bold Text", markup);
}
[Test]
public void TestMarkdownMultipleBoldText()
{
var markup = marklet.Markup("Some ##1## ##2## Text");
Assert.AreEqual("Some <strong>1</strong> <strong>2</strong> Text", markup);
}
A large 1,000+ line class can be shrunk down to a collection of 10 line classes.
Extensibility can also be achieved through an Inversion of Control container, meaning changes can be made without the need to re-compile.
1 Comments:
Any chance you plan to open the code? Or did I miss a link somewhere? Thanks.
Post a Comment
Subscribe to Post Comments [Atom]
<< Home