What is DocFx? Itās a static site generator mainly used for creating API documentation for your code. But it can be used for any static sites. We use this for the Lucene.Net projectās website and documentation. The end result is API docs that look and feel a little bit familiar, kind of like Microsoftās own API documentation website. Iām not entirely sure if their docs are built with DocFx but I suspect it is but with some highly customized builds and plugins ⦠but thatās just my own assumption.
Speaking of customizing DocFx, it is certainly possible. That said the ironic part about DocFx is that itās own documentation is not great. One of the markdown customizations we needed for the Lucene.Net project was to add a customized note that some APIs are experimental. This tag is based on the converted Java Lucene docs and looks like: ā@ lucene.experimental ā. So we wanted to detect that string and convert it to a nice looking note similar to the DocFx markdown note. Luckily there is some docs on how to do that although theyāre not at all succinct but the example pretty much covers exactly what we wanted to do.
Block markdown token
This example is a block level token since it exists on itās own line and not within other text. This is also the example DocFx provides in itās docs. Itās relatively easy to do:
- Register a IDfmEngineCustomizer to insert/add a āBlock Ruleā
- Create a new āBlock Ruleā which in itās simplistic form is a regex that parses the current text block and if it matches it returns an instance of a custom āTokenā class
- Create a custom āTokenā class to store the information about what youāve parsed
- Create a custom āRendererā to write out actual HTML result you want
- Register a IDfmCustomizedRendererPartProvider to expose your āRendererā
This all uses MEF to wire everything up. You can see the Lucene.Net implementation of a custom markdown block token here: https://github.com/apache/lucenenet/tree/master/src/docs/LuceneDocsPlugins
Inline markdown token
The above was āeasyā because itās more or less following the DocFx documentation example. So the next challenge is that I wanted to be able to render an Environment Variable value within the markdown⦠sounds easy enough? Well the code result is actually super simple but my journey to get there was absolutely not!
Thereās zero documentation about customizing the markdown engine for inline markdown and thereās almost zero documentation in the codebase about what is going on too which makes things a little interesting. I tried following the same steps above for the block markdown token and realized in the code that itās using a MarkdownBlockContext instance so I discovered thereās a MarkdownInlineContext so thought, weāll just swap that out ⦠but that doesnāt work. I tried inserting my inline rule at the beginning, end, middle, etc⦠of the DfmEngineBuilder.InlineInlineRules within my IDfmEngineCustomizer but nothing seemed to happen. Hrm. So I cloned the DocFx repo and started diving into the tests and breaking pointing, etcā¦
So hereās what I discovered:
- Depending on the token and if a token can contain other tokens, its the tokens responsibility to recurse the parsing
- Thereās a sort of ācatch allā rule called MarkdownTextInlineRule and that will āeatā characters that donāt match the very specific markdown chars that itās not looking for.
- This means that if you have an inline token that is delimited by chars that this doesnāt āeatā, then your rule will not match. So your rule can only begin with certain chars: \<!\[*`
- Your rule must run before this one
- For inline rules you donāt need a āRendererā (i.e. IDfmCustomizedRendererPartProvider)
- Inline rule regex needs to match at the beginning of the string with the hat ^ symbol. This is a pretty critical part of how DocFx parses itās inline content.
Now that I know that, making this extension is super simple:
- Iāll make a Markdown token: [EnvVar:MyEnvironmentVar] which will parse to just render the value of the environment variable with that name, in this example: MyEnvironmentVariable.
- Iāll insert my rule to the top of the list so it doesnāt come after the catch-all rule
// customize the engine
[Export(typeof(IDfmEngineCustomizer))]
public class LuceneDfmEngineCustomizer : IDfmEngineCustomizer
{
public void Customize(DfmEngineBuilder builder, IReadOnlyDictionary<string, object> parameters)
{
// insert inline rule at the top
builder.InlineRules = builder.InlineRules.Insert(0, new EnvironmentVariableInlineRule());
}
}
// define the rule
public class EnvironmentVariableInlineRule : IMarkdownRule
{
// give it a name
public string Name => "EnvVarToken";
// define my regex to match
private static readonly Regex _envVarRegex = new Regex(@"^\[EnvVar:(\w+?)\]", RegexOptions.Compiled);
// process the match
public IMarkdownToken TryMatch(IMarkdownParser parser, IMarkdownParsingContext context)
{
var match = _envVarRegex.Match(context.CurrentMarkdown);
if (match.Length == 0) return null;
var envVar = match.Groups[1].Value;
var text = Environment.GetEnvironmentVariable(envVar);
if (text == null) return null;
// 'eat' the characters of the current markdown token so they aren't re-processed
var sourceInfo = context.Consume(match.Length);
// return a docfx token that just returns the text passed to it
return new MarkdownTextToken(this, parser.Context, text, sourceInfo);
}
}
In the end, thatās actually pretty simple! But donāt go trying to create a fancy token that doesnāt start with those magic characters since itās not going to work.