.NET and Asciidoc


12/28/2024

Some years ago, Asciidoctor launches a documentation revolution bringing the power of LaTeX with the simplicity of Markdown . High level the idea was to make a wiki like syntax very powerful and extensible. Let see what it means in .NET land.

Why not Markdown?

Markdown is great when you write simple content but as soon as you start writing serious content (documentations, blog sites, ...) you hit the fact that every implementation has a custom flavor of markdown, in other words, there is no "markdown" there are "markdowns".

The basic syntax - original one - is quite portable but it is limited to titles, paragraphs, line breaks, bold/italic formatting, quotes, lists (ordered or not), simple code blocks (no language), images, links.

Some important lacks are - for example - tables, definition lists, admonitions (warnings, tips, notes, ...) and more.

This is why Github uses an extended Markdown markup, that CommonMark and others were created: to add what was missing to be complete to raw Markdown and make it usable.

The main issue is that it is difficult to:

  1. Create some knowledge sharing between people in this context since depending what solution you work on you learn something different,

  2. Create some transversal tooling working on all implementations (linting for example to guarantee the quality of the writing format).

For that reason using asciidoc which defines more generally an unified syntax is generally more relevant even if as soon as you start using implementation you always have a few glitches but you don't start by glitches and implementations tend to fix issues so you will be able to get knowledge sharing and pipeline automotion which are worth it a lot.

Asciidoctor: one code to rule them all?

Asciidoctor is a ruby project. It aimes at implementing Asciidoc markup (which was originally implemented in python).

A few specificities of Asciidoctor were to push to make the syntax more efficient but also to use portability of ruby code base to make the implementation available for multiple lanaguages. This is how Javascript got Asciidoctor (thanks Opal ) and Java got Asciidoctor using JRuby .

The issue is that the ruby support accross languages is not that spread and is acually slow in most languages (in particular JRuby is a great runtime but a poor solution for fast rendering since the interpreter and warmup time are not that fast so Java community was not that happy about that solution). The other issue making it "portable" is that you need to embrace the stack of the "portability" layer, it all dependencies of JRuby for example for Java case and it is way more than what you expect.

So while this enabled people to start to do Asciidoc everywhere, it still had some issues and .NET was ont even enabled.

NAsciidoc

NAsciidoc is the .NET conversion of Yupiik Asciidoc-java bundle which was a reimplementation from scratch of an Asciidoc(tor) parser and renderer (HTML - almost - compatible, ie you can reuse the CSS if you want).

It is .NET native and doesn't suffer from having to integrate with native libraries (it is "dotnet portable") or OS dependencies.

In terms of usage there are three main area in the core bundle/nuget (NAsciidoc.Core):

  • The AST model: it enables you to have in memory the whole document (the title, the lists, the paragraphs, etc...),

  • The parser: it enables to load the model (often a Document or Body when used as partial rendering),

  • The renderers: here the design was to have a visitor of the AST model to enable to change the renderer by plain decoration which avoids to have extension point for all potential cases (which are all the elements of the AST + the combinations of their attributes....it is a lot since it is infinite).

A sample usage will look like:

var parser = new Parser(); (1)

var context = new ParserContext(new LocalContentResolver("path/to/workspace")); (2)
var doc = parser.Parse( (3)
  """
    = My Adoc

    With some content.
    """,
  context
);

var conf = new AsciidoctorLikeHtmlRenderer.Configuration(); (4)
var renderer = new AsciidoctorLikeHtmlRenderer(conf); (5)
renderer.Visit(doc); (6)
var html = renderer.Result(); (7)
  1. Create a parser, it is a thread safe instance you can reuse,
  2. Create a parser context, it enables to resolve includes and potentially some parse time attributes,
  3. Load the AST model in memory by parsing some content,
  4. Create a renderer configuration (depends the renderer/visitor you want to use, the sample uses Asciidoctor compatible one) - take care to pass the configuration you need if any like the rendering attributes etc..,
  5. Instantiate your renderer,
  6. Visit the document to do the actual rendering,
  7. And finally get the rendered result.

Strength of this model

The strength of this model is that you use this pipeline: write → load → render.

It means you can easily convert it to: write → load → modify the model → render.

And also: write → load → custom rendering.

The most advanced pipeline - you can set up on Github Action or any CI - will likely be: write → load → modify the model → validate/lint the model → custom renderings (HTML, PDF, ebook, ...).

Custom rendering

The decorator pattern for the rendering is likely the most powerful, for example if I want a standard asciidoctor renderer but with Twitter Bootstrap alter blocks for admonitions, i can just extend or decorate (decoration is "cleaner" in terms of implementation, extending the base renderer is less verbose) the default visitor and override the admonition visitor:

public class BootstrapRender(AsciidoctorLikeHtmlRenderer.Configuration configuration)
    : AsciidoctorLikeHtmlRenderer(configuration)
{
    public override void VisitAdmonition(Admonition element)
    {
        string name = Enum.GetName(element.Level)!;
        builder.Append(
            Div(
                role: "alert",
                className: "alert alert-" + element.Level switch
                {
                    Admonition.AdmonitionLevel.Note => "info",
                    Admonition.AdmonitionLevel.Tip => "success",
                    Admonition.AdmonitionLevel.Important => "primary",
                    Admonition.AdmonitionLevel.Warning => "warning",
                    Admonition.AdmonitionLevel.Caution => "danger",
                    _ => "secondary"
                },
                children: [
                    H4(className: "alert-heading", children: B($"{char.ToUpperInvariant(name[0])}{name[1..]}")),
                    RenderChildren(element.Content)
                ]
            ));
    }
}

Conclusion

Documentation the code we write is likely more important than writing any code, but being able to work on the documentation on the long run and sharing work with others is as important.

In that context, asciidoc tends to be the best compromise since it couples the simplicity of a markup language with the power and extensibility of the more advanced languages related to writings (documentations, books, blogs, ...).

And by the way, this blog is powered by SBSharp which is backed by NAsciidoc and a Github Action to publish updates.


rmannibucau
Tech Lead/Software Architect, Apache Software committer, Java/Js/.NET guy

LinkedIn GitHub