Interesting documentation

Now those are two words you don’t often hear put together, but I think what we came up with for NSubstitute’s documentation is actually pretty interesting, and so I thought I’d share it. I’m not talking about the documentation itself, but about ways to write documentation that is maintainable and helps out your project as well as your users. In our case our docs ended up fairly easy to write, automatically tested for correctness and generated as HTML as part of the build. And this makes me happy, and so I blog. :)

Creating a monster of a website with Jekyll

We wanted to publish the NSubstitute docs to the website. We were pretty keen to be able to write the docs in Markdown or similar, as writing large wads of HTML can be quite annoying; especially the hassle of escaping code samples. It turns out there are a number of static website generators that support this kind of approach (the site for one of these generators, nanoc, has a good list of a number of these, mostly Ruby, but also some done in Python, Haskell and others).

For NSubstitute we ended up choosing Jekyll, a generator done in Ruby that is really optimised for blogs (and is also used for GitHub Pages). We tried a few other options that were very good too, but Jekyll seemed to fit in closest with what we wanted without too much effort.

Jekyll works mainly from conventions with directory and file names. The root of the docs site includes a _config.yml file to include information like permalink format; layout templates go in _layouts; and posts typically go in _posts with the filename in the form yyyy-mm-dd-title.markdown. In NSubstitute we’re not too worried about the date itself, but Jekyll insists on it (it is designed for blogs remember), and it does provide an easy way of ordering posts. You can also put posts/documents under different categories, and include normal HTML pages and content.

After running the jekyll command on your docs directory, out pops a static site. For rapidly testing changes you can also run jekyll --server --auto to serve up and automatically-regenerate the site every change on http://localhost:4000. It is hard to capture how awesome this is in a post, but compared to other attempts I’ve had at getting quick sites up and running I found this amazing.

Liquid templates

Jekyll uses Liquid (and Liquid Extensions) for templates. In the Jekyll _layouts files you can put in markup like {{ page.content }} and {% for post in site.categories.help reversed %}, {% endfor %} and these will be expanded as required in the generated site.

What’s really neat is that, in addition to the large range of built in tags and templates, it is also frightfully easy to create your own in Ruby. You just put them into a _plugins directory and Jekyll will automatically pick them up and apply them when generating your site.

For NSubstitute we wanted to easily include code samples formated using SyntaxHighlighter, so we created an {% examplecode %} tag:

module Jekyll
 class ExampleCodeBlock < Liquid::Block
        include Liquid::StandardFilters
  def render(context)
            code = super.join
            <<-HTML
<div class="highlight">
    <pre class="brush: csharp">#{h(code).strip}</pre>
</div>
            HTML
  end
 end
end
Liquid::Template.register_tag('examplecode', Jekyll::ExampleCodeBlock)

Any code samples wrapped in {% examplecode %}, {% endexamplecode %} tags will be automatically escaped (h(code) is built in to Liquid to do this) and will be wrapped in an element that includes the class="brush: csharp" that SyntaxHighlighter needs to do its job.

Note: Jekyll has built in support for syntax-highlighting using Pygments. Pygments is awesome too, but for a number of reasons (mainly ease of setup) we chose to use SyntaxHighlighter instead.

Docs with teeth

We wanted to make sure the NSubstitute code examples worked, and kept working even with future versions. It’s pretty frustrating to plug in an example from somewhere and have it not work. To use Anthony’s expression, we wanted “documents with teeth”; docs that would bite us if we did the wrong thing.

Writing runnable code samples

I’ve already mentioned we included code samples, written in Markdown, and designated with {% examplecode %} Liquid blocks. This would make it pretty easy to parse out these blocks and get them into a C# file so we could compile them, as we could just grab the text between the blocks without HTML decoding. The idea was we’d wrap each example in an NUnit test and make sure they both compile and any assertions passed.

But what about supporting code that may be required, but that we didn’t necessarily want the reader to be bothered with? We may not want to repeat setup code for every example on a page, that would just add a lot of noise to the examples. So just as for our {% examplecode %} tag, we created a {% requiredcode %} tag that wouldn’t render its contents to the final page, but could still be parsed out into our C# files.

The last hurdle was how to handle code-blocks that included class or interface definitions. These couldn’t just go into an NUnit test method as the C# compiler isn’t too fond of class definitions going into a method body. So we needed our parser to check if {% examplecode %} contained a class or interface definition, and it it did, then just dump it in the C# file without wrapping it in a [Test] method.

Code samples to code files to tested DLL

Now we had code samples in our documents, we needed to get them into C# files we could compile. This we ended up driving out in my own particular brand of n00bie Ruby, the results of which you can see on Github (with tests in ../specs). The ExamplesToCode class takes a documentation input path and an output path for our class files, extracts all the code blocks found in the input, then shoves each block into a template for a C# file either as a declaration or as a test and writes it to the output path.

This is then called from our rake script like this:

task :generate_code_examples do
    examples_to_code = ExamplesToCode.create
    examples_to_code.convert("../Source/Docs/", "#{OUTPUT_PATH}/CodeFromDocs")
    examples_to_code.convert("../Source/Docs/help/_posts/", "#{OUTPUT_PATH}/CodeFromDocs")
    examples_to_code.convert("../", "#{OUTPUT_PATH}/CodeFromDocs")
end

We compile these using csc.exe, and run the tests in the resulting DLL using NUnit:

task :compile_code_examples => [:generate_code_examples] do
    #Copy references to documentation directory
    output_base_path = "#{OUTPUT_PATH}/#{CONFIG}"
    output_doc_path = "#{OUTPUT_PATH}/CodeFromDocs"
    references = %w(NSubstitute.dll nunit.framework.dll).map do |x|
        "#{output_base_path}/NSubstitute.Specs/#{x}"
    end
    FileUtils.cp references, output_doc_path

    #Compile
    FileUtils.cd output_doc_path do
        sh "#{DOT_NET_PATH}/csc.exe /target:library /out:\"NSubstitute.Samples.dll\" /reference:\"nunit.framework.dll\" /reference:\"NSubstitute.dll\" *.cs"
    end
end

Ok, so it’s not pretty, but it works a treat.

Generating the site

The last step was to automate all of this: generating code samples, testing them, then generating the final site for publishing.

desc "Generates documentation for the website"
task :generate_docs => [:all, :check_examples] do
    output = File.expand_path("#{OUTPUT_PATH}/nsubstitute.github.com", File.dirname(__FILE__))
    FileUtils.cd "../Source/Docs" do
        sh "jekyll \"#{output}\""
    end
end

Conclusion

This has been a whirlwind tour around NSubstitute’s documentation systemy thang; mentioning things like Jekyll, Liquid, Markdown, some custom Ruby libraries and a couple of rake tasks. What this gives us (and devs using NSubstitue) is fairly high confidence that any code samples on the site are going to work exactly as advertised.

This has been an incredibly worthwhile experience, not only for making the documentation useful, but also in discovering great tools like Jekyll (which I am so tempted to migrate this blog to :)).

As parting words for an excessively rambling post (even for me! :)), if you need to write some documentation for anything involving code, try and get some value out of it. Make it living documentation that will bite you when you make a mistake, and will help itself stay up-to-date by failing a build if it is neglected. Approached this way, documentation is even kind of fun. So go forth, all ye developers, and document! :)

Comments