Markdown Lite
Introduction
Markdown lite is a simple markdown tool to markup md
file.
Design goal
We write this tool for good extensibility, so our implementation should obey following principles:
- Extensibility:
- Support markdown syntax extension.
- Support validation extension.
- Correctness: We follow GFM syntax, but when some rules is too hard to implement, just breaking.
- Performance: Performance is not our major concern.
Steps
There are three steps when calling markup method:
Step 1: Parse
In this step, it will parse markdown text to tokens. The parser is based on rules, which make up the context.
For example, heading token is created by heading rule, the heading rule is belonging to block context.
Step 2: Rewrite or validate
In this step, it will walk through all tokens, we can change it to another, or just validate.
For example, we can create a rewriter to change all heading token with depth + 1:
MarkdownTokenRewriterFactory.FromLambda<IMarkdownRewriteEngine, MarkdownHeadingBlockToken>(
(engine, token) => new MarkdownHeadingBlockToken(token.Rule, token.Context, token.Content, token.Id, token.Depth + 1, token.SourceInfo);
Step 3: Render
In this step, it renders models to text content (html format by default). To simplify extension, we created an adapter, the adapter invoke methods by following rules:
- Method name is
Render
- Instance method
- Return type is StringBuffer
- The count of parameters is 3, and types are following:
- IMarkdownRenderer or any type implements it.
- IMarkdownToken or any type implements it.
- IMarkdownContext or any type implements it.
- Always invoke the best overloaded method (The best is defined by binder).
Engine and engine builder
Engine is a set of parser, rewriter and renderer. It can markup a markdown file to html file (or others). But it cannot be invoked in parallel.
So we create an engine builder. It defines all the rules of parser, rewriter and renderer. It can create instances when needed.
How to customize markdown syntax
Define markdown syntax
Define markdown:
: My label
should be rendered as following html:
<div id="My label"></div>
Select token kind
First of all, we should select the context for this rule.
And in this goal, the new line is required.
So it should be a block token, all of the names for class should contain Block
.
Define token
Create a token class like following:
public class MarkdownMyLabelBlockToken : IMarkdownToken
{
public MarkdownMyLabelBlockToken(IMarkdownRule rule, IMarkdownContext context, string label, SourceInfo sourceInfo)
{
Rule = rule;
Context = context;
Label = label;
SourceInfo = sourceInfo;
}
public IMarkdownRule Rule { get; }
public IMarkdownContext Context { get; }
public string Label { get; }
public SourceInfo SourceInfo { get; }
}
Define rule
Create a rule class as following:
public class MarkdownMyLabelBlockRule : IMarkdownRule
{
public virtual string Name => "My Label";
public virtual Regex LabelRegex { get; } = new Regex("^\: *([^\n]+?) *(?:\n+|$)");
public virtual IMarkdownToken TryMatch(IMarkdownParser parser, IMarkdownParsingContext context)
{
var match = LabelRegex.Match(context.CurrentMarkdown);
if (match.Length == 0)
{
return null;
}
var sourceInfo = context.Consume(match.Length);
return new MarkdownMyLabelBlockToken(this, parser.Context, match.Groups[1].Value, sourceInfo);
}
}
Define renderer
Create a renderer class as following:
public class MyRenderer : HtmlRenderer
{
public virtual StringBuffer Render(IMarkdownRenderer renderer, MarkdownMyLabelBlockToken token, IMarkdownContext context)
{
return StringBuffer.Empty + "<div id=\"" + token.Label + "\"></div>";
}
}
Define engine builder
Create an engine builder class as following:
public class MyEngineBuilder : GfmEngineBuilder
{
public MyEngineBuilder(Options options) : base(options)
{
BlockRules = BlockRules.Insert(0, new MarkdownMyLabelBlockRule());
}
}
Markup it!
Then use your custom markdown in your code:
public string MarkupMyMarkdown(string markdown)
{
var builder = new MyEngineBuilder(new Options());
var engine = builder.CreateEngine(new MyRender())
return engine.Markup(markdown);
}