Summary
GF2++ is a header only C++ library for performing linear algebra with boolean vectors and matrices (more correctly in mathematical jargon, for linear algebra over GF(2)).
Even if you have no interest in the mathematics in question, or in C++, you may still find the discussion of how we documented this small library to be of some use.
The library is documented in two ways:
Brief in-source documentation is provided for all the publicly accessible methods, functions, and data members. This is done in the usual manner using code comments that are formatted with a minimal set of Doxygen commands/tags. Modern code editors can parse those specially formatted comments and use them to present the library user with some useful tool tips to remind them of method arguments etc.
The library also has long form documentation which is maintained external to the source itself though it does live in the same code repository. This documentation is written in AsciiDoc, a markup language that is richer than the ubiquitous Markdown but still pretty light weight. AsciiDoc provides several features that make it ideal for generating technical documentation that spans many pages. Our AsciiDoc pages include small copyable example programs that exercise all the key functionality of the library.
The site generator Antora marries those AsciiDoc content files with others that define a look and feel to produce the static documentation website.
You can navigate through the documentation strategy document in sections by clicking through the navigation menu on the left or as a single (long) document here.
The Problem
Introduction
Documenting code is notoriously hard to do well.
Many years ago now, Donald Knuth introduced the idea of literate programming, where code essentially exists as marked up snippets inside an essay that expounded the algorithms being developed by the programmer.
Instead of imagining that our main task is to instruct a computer what to do, let us concentrate rather on explaining to human beings what we want a computer to do.
Knuth’s emphasis was on the essay part of the puzzle. Very fitting for a distinguished author such as himself with numerous seminal and beautifully produced textbooks to his name.
However, most programmers aren’t all that good at writing a small number of lines of code, let alone a long form exposition of some sort!
Nevertheless, the idea of code as an essay lives on and flourishes these days in many popular environments like Jupyter notebooks, Mathematica notebooks, Maple sheets etc. There are even collaborative tools that allow a group of authors to work on a common notebook without clobbering each other’s work. However, Knuth’s idea of one source both getting tangled into efficient code for optimal execution and also woven into beautiful documentation gets lost in those systems. In the hands of good authors, notebooks can be great at exposition but they generally do not compose well into larger systems.
Code libraries of one form or another remain the main tools that are deployed to build those larger systems. And libraries are really only widely used if they are properly documented.
Most coders do learn to occasionally plop a comment line or two into their code. There have been various attempts to standardize the format of those code comments with the aim of easily extracting them into a form of readable documentation.
At first blush, this seems like a reasonable goal. The library author concentrates for the most part on writing code but she also learns a little extra syntax which is used to slightly enrich and, more importantly, standardize the way comments are written—particularly those at, or near, the start of major code blocks like classes, methods, functions, etc. The compiler ignores all comments anyway so a separate tool extracts the specially formatted comments and uses those, perhaps along with other information from the compiler, to automatically generate documentation.
The emphasis here is the opposite of Knuth’s. You mainly write code and hopefully embed enough comments to document the intent and use of that code. The main advantage is that the comments live right in the source itself so have a meaningful chance of remaining up to date and relevant as the code evolves.
Tool Tips
One advantage of using specially marked up comments as documentation is that modern code editors will parse them and make them available when a programmer imports a library module or header file. She can then just hover over a library provided function call and get a nice tool tip to remind her of what the relevant arguments should be etc.
This tool tip use of formatted comments is by itself quite useful and worth the price of admission.
The library writer provides some brief standardized comments which are not too onerous to create.
For example, in the class GF2::Matrix we have a identity(…) method that begins as follows:
/// @brief Factory method to generate the identity bit-matrix.
/// @param n The size of the matrix to generate.
/// @return An `n x n` bit-matrix with 1's on the diagonal and 0's everywhere else.
static constexpr Matrix identity(std::size_t n) {
...
}
Here we are using comments marked up in one of the formats understood by one of the best know documentation generators Doxygen.
Doxygen is itself a sizable system and one with a large number of possible directives.
However, if you just want to provide some useful tool tips you don’t need to understand anything about Doxygen at all (or even have it installed) and really only need to know a handful of fairly obvious directives—the three used above @brief, @param, and @return already cover a lot of ground and their purposes are pretty obvious!
This works because parsing trivial Doxygen markup is built in to most modern code environments either directly or through a plugin. For example, if you are editing code that calls that method in VSCode, hovering over the appropriate spot produces a tool tip like:
The tip is slightly wordy because GF2::Matrix is a template class (hence that <Block, Allocator> reference) but nonetheless, it does an adequate job of explaining what’s going on.
This particular method is so simple that it might be argued that any documentation at all is a bit of an overkill but we took the view it is better to be consistent and to provide this minimal level of documentation in-source for all public methods and functions in our library.
However, good documentation goes a lot further than just providing tool tips.
Introductory material may be needed, the rationale for a particular implementation given, gotchas enumerated, fallbacks enumerated if preconditions are not met, etc. Above all, short focused examples showing how a method might be used and what it might return are generally really useful for the end-user of a library. This is the material you expect to see in a professional library’s long form documentation site all nicely laid out and correctly linked together in a user friendly format.
Doxygen was built to handle all of that and there are projects which use it. However, putting all that material into the source code itself just seems clunky and pushes code commenting a bit too far. Apart from anything else, the number of comment lines balloons to the point where it is hard to scan the code itself. Writing a lot of material as comment blocks isn’t all that pleasant an experience either though there are plugins for editors that attempt to ease the pain.
Of course Doxygen can link in documents that are independent of the source code but at that point you start to lose the benefit of having one source for the code and its documentation.
The C++ Standard Library
If you browse through the headers of the standard library on your computer (or more likely get sucked into looking at one of those headers when your IDE finds an error in your code that leads you down to the dungeon) you quickly realize that these pearls of computer wisdom are nigh on unreadable!
The only sizable comments in the headers are often limited to some legal gobbledygook pertaining to licensing at the start of the file. Variables are tersely named and festooned with underscores that hurt the eye (those visually unappealing decorations are there to stop you shooting yourself in the foot by some inadvertent misuse of the preprocessor). Even if you can get past those festering carbuncles you will still likely be confounded by the sheer generality of the code which is expected to handle obscure corner cases and ancient computer architectures. That is all part of the genius & generality of the library but it certainly doesn’t make for a good read!
Of course if you are happily typing away in any reasonable IDE you will remain oblivious to the warts and instead get excellent tool tips for all standard library objects and calls. However, those do not come from comments embedded in the header files but instead are provided using some other mechanism. Your IDE will probably also provide links to really good long form documentation for the standard library complete with all the goodies we mentioned earlier.
For C++ developers, one long form documentation site really stands out, namely CPP Reference. To quote from their FAQ, the purpose of the site is as follows.
Our goal is to provide programmers with a complete online reference for the C and C++ languages and standard libraries, i.e. a more convenient version of the C and C++ standards. The primary objective is to have a good specification of C and C++. That is, things that are implicitly clear to an experienced programmer should be omitted, or at least separated from the main description of a function, constant or class. A good place to demonstrate various use cases is the “example” section of each page. Rationale, implementation notes, domain specific documentation are preferred to be included in the “notes” section of each page.
Quality is valued over quantity so the site sticks to just documenting the standard library, tempting though it might be to add things like some of the most popular Boost libraries.
To be fair, the site has a very particular look which might not be to everyone’s taste. For one thing, it somehow doesn’t look all that “modern” as there is a lot of text on every page all in a single undistinguished typeface ( DejaVuSans ) and to boot, some of that text is in quite small point sizes.
Nevertheless, the quality of the documentation is excellent and the writing style and format is consistent across the entire large project which is quite the task given that the whole thing is a community effort (it makes me suspect that a huge amount of the material is actually written or at least heavily edited by a relatively small number of contributors).
In many ways CPP Reference sets the gold standard for documentation of a C++ library. So emulating it would seem like a reasonable approach to documenting a new library. Achieving a comparable writing style is of course up to the author but unfortunately mirroring the page format turns out to be quite difficult.
CPP Reference is a wiki built using the same MediaWiki software that powers Wikipedia. While the basic markup used isn’t all that hard, everything in the site is wrapped in “quite complex templates”. The editors basically suggest that a contributor just concentrates on writing some content and then let the experts take over to put the text into the correct format.
Moreover, the MediaWiki software was designed to be used in a browser which is very clunky and basic compared to using any fully featured modern editor.
AsciiDoc
Introduction
Lightweight Doxygen comments embedded in the library source code is an excellent way to provide short-form, tool-tip style documentation. On the other hand, long form documentation complete with cross links, references, examples etc., is best built and maintained outside the source code itself though most likely in files that are located in the same repository.
What markup format should those long form pages use?
Markdown is by far the best known lightweight markup language.
These days it is used just about everywhere on the web.
It is so simple to learn that you can get up to speed in just a couple of minutes, yet it provides enough features to enable you to write most shorter documents.
It has chapters/sections, paragraphs, the usual text formatting such as italic, bold, monospaced etc., along with lists, tables, and so on.
Code blocks can be made stand out from regular text which is handy for documentation purposes.
The format is ubiquitous so if you download a Markdown file to your tablet or computer and click on it the chances are excellent that it will open and get rendered in some very reasonable way (though not necessarily the exact same way that it looks to someone reading it on a different device or application).
Markdown is just simple text with some character combinations used to suggest formatting. If you can write text, you can write Markdown. Moreover, there are a huge number of tools available that enable you to view the formatted output more or less as you type it. Every editor will have a plugin that renders Markdown. There are also some excellent dedicated Markdown editors.
Of course, our GF2++ is a math library so it is handy to be able to have a formula or two in the documentation.
That wasn’t part of the original Markdown but these days most flavors allow you to embed \(\TeX\) equations like:
However, even with all that, Markdown is far from ideal for writing software documentation. It shines when everything fits in a single file but it misses some key features once a project spills over to many files. Links between files can be created in Markdown but you can’t really even simply include one file or part of a file in another (for example have a code snippet in one file and then include the relevant section as text into another).
There is also no native support for some really useful things that are common in technical documentation. For example, callouts where you highlight lines of sample code with explanatory materials, and admonitions where you warn the user of some particular issue or other. Of course Markdown does allow you to embed HTML in the document so all things are possible but, once you go that route, definitely no longer easy.
As the documentation grows you will inevitably be led to exploring alternatives and the one we settled on is AsciiDoc.
AsciiDoc itself can be a pretty lightweight markup language.
In fact, if you restrict yourself to the same feature set as Markdown, an AsciiDoc .adoc file will look very similar to the equivalent Markdown .md one.
Here is a cheat-sheet that shows the equivalence between the two forms of markup.
However, AsciiDoc is extensible and much more powerful than Markdown.
For example, the AsciiDoc include directive allows you to embed some snippets of text in multiple locations without resorting to copy-paste.
Explanations can easily be added to particular code lines by using callouts such as:
auto v = GF2::Vector<>::random(16); (1)
| 1 | This creates a GF2::Vector of size 16 with a random fill where the vector elements are equally likely to be 0 or 1. |
AsciiDoc also has admonitions as a way to highlight warnings so the reader’s attention is drawn to particular gotchas that might trip them up. Those can also be used to highlight tips and informational notes etc. such as:
| These features and many others are standard in AsciiDoc and very simple to use. |
AsciiDoc is not nearly as well known as Markdown but nonetheless your editor probably has a plug-in to make writing it pleasant and to enable previewing the formatted output as you go.
Asciidoctor
These days, Asciidoctor is the principal tool used to process AsciiDoc files. It is a mature open source program that can convert AsciiDoc to HTML, PDF, EPUB, DocBook, manual pages etc. It is written in Ruby and works on all the usual platforms, Windows, macOS, and Linux.
There is also a JavaScript version so you can install a browser extension and then simply open an AsciiDoc file to see it nicely rendered with active links etc. That can be quite handy when you are authoring material as you can see previews in a browser even if your editor doesn’t have any specific AsciiDoc support.
Of course you can hardly expect all your library users to install a browser plugin just to read your documentation!
Instead you might use Asciidoctor to convert all the .adoc files into HTML.
Suppose the AsciiDoc content is in files and subdirectories sitting beneath a documentation/manual/pages/ directory.
Then you can sit in the parent documentation/manual/ and use the the following Asciidoctor command to convert all the .adoc files in pages/ into their .html equivalents.
$ asciidoctor -R pages -D html pages/**/*.adoc
The .html files will in be found in an identical directory layout in the sibling documentation/manual/html/ directory.
A reader can then browse through those HTML files without any need for an AsciiDoc plug-in. However, all Asciidoctor gives you is a simple tree of files without any of the navigation niceties you would expect in a modern website so the job is not yet done!
Antora
Introduction
Like its simpler Markdown cousin, AsciiDoc is a readable markup language that is easy to create in any text editor. As we explained, it is primarily aimed at creating technical content. The Asciidoctor program can transform that content into various output formats amongst which is HTML, the language of the web.
If you have run asciidoctor as shown here then you can open the files in documentation/manual/html/ directory and read the documentation in a browser from there.
No plug-in will be required as you are reading HTML rendered nicely by your browser.
However, Asciidoctor is not a web-site builder. So while the styling will be good, the documentation tree is still just that — a simple tree of files without any navigation bar, menus, search box etc. so not a real website. Navigation is limited to the forward and back buttons in your browser.
Antora aims to fill the void. It is a static website generator that creates sites where the content is written in AsciiDoc and where the corresponding files are stored in a version control system like git. It is used to add elements like menus, navigation bars, search boxes, etc. — elements we expect to see when we visit a website.
Antora has been designed to facilitate very large scale technical documentation projects where the content might be spread across many different repos. In fact it can be used to consistently document a complete operating system with thousands of libraries and hundreds of applications. Not only that, its design means it can gracefully handle different versions of each of those through their lifecycle.
This generality is both a blessing and a curse!
While there is copious Antora documentation, it tends to focus on the great flexibility of the system without nearly enough reference to specific simple complete examples. The suggested use cases are quite heavy weight with lots of documentation in different repositories and possibly even a team of people with specialized technical writing roles and so on. Even the Antora Quick-start isn’t all that useful as it relies on existing content pulled from other repositories instead of building a small site from scratch in a single repository. This all makes Antora appear quite daunting.
Of course it is great that the system was designed to scale up to the very large but it is quite hard to get started with a much simpler scenario like documenting our little header-only library GF2++.
Well we do know there are couple of requirements up front:
-
The documentation must be written in AsciiDoc.
We’ve already talked about the advantages of AsciiDoc as a markup language so we’ll take that as a given. -
The documentation must be stored somewhere in a git repository.
Using git to keep track of changes to documentation in the same way it is used to track changes in a code base is generally a good thing but in fact the git repository can be a completely local and even pretty bogus. Assuming you havegitinstalled, from the root directory of your project you can just do:git init . git commit --allow-empty -m 'Created repository'
That’s it! You have now got a local git repository that Antora will be happy to work with even though you haven’t checked in anything to it.
Sample Project
Let’s focus on a simple scenario where you are building an application FabulousApp whose code base sits in a single repository and which is documented with an installation manual and a user guide.
In broad strokes the FabulousApp repository might be laid out along the following lines:
FabulousApp ├── .git/... (1) ├── docs (2) │ ├── installation-manual/... (3) │ └── user-guide/... ├── src (4) │ ├── FabulousApp.cpp │ └── ... ├── README.adoc (5) ├── .... (6)
| 1 | FabulousApp is a git repository. |
| 2 | As is common practice, the documentation for FabulousApp is stored in a docs/ directory at the root of the repo. |
| 3 | The installation-manual and user-guide are in their own directories and will have content files written in AsciiDoc. |
| 4 | In this example the source code for FabulousApp is in the same repo as its documentation. |
| 5 | As an aside we note that GitHub and friends all natively render AsciiDoc so you can use its more powerful format for all those README files! |
| 6 | A real project probably has lots of other files and directories for build systems, test cases, deployment logs, etc. |
We now want to use Antora to build a website that hosts that documentation in a decent looking, easily navigable website.
In common with most static website generators, Antora expects that the content for a documentation website is laid out in a particular structure of directories and files. Because Antora is designed to potentially handle very large documentation projects that structure might be a bit deeper than you might think strictly necessary.
| You can use Antora symlinks to keep the documentation source files in more natural locations closer to the root of the repository. We do that in the sample setup below and also in our own repository. |
Antora Modules
There are a couple of Antora concepts that need to be understood to make sense of the Antora’s required documentation layout, namely that of an Antora module and an Antora component.
The first of those is pretty natural — a module is simply any block of documentation that sits together in a single directory.
So in our FabulousApp example there are two modules — the installation manual and the user guide. The content for each module is stored in its own directory — of course there are ways to link and even include content from other modules as needed.
That all seems very unobjectionable.
However, in order for Antora to work, the module directories must be structured in a specific manner.
In particular, the AsciiDoc content in the module needs to be down one level in the module’s pages/ subdirectory.
For example our docs/user-guide/ needs to be laid out like:
FabulousApp ├── ... ├── docs │ ├── ... │ └── user-guide │ ├── pages/... (1) │ ├── images/... (2) │ ├── ... (3) │ └── nav.adoc (4) ├── ...
| 1 | All of the AsciiDoc content needs to be in the pages/ subdirectory. |
| 2 | Any image files you use in the documentation goes in this sibling images/ subdirectory. |
| 3 | There other specifically Antora named subdirectories for other types of content. |
| 4 | The nav.adoc is basically an AsciiDoc list that tells Antora how to navigate the content. |
The pages/ subdirectory will usually be the main event for the module and will have the bulk of the written content.
Within the pages/ directory you can organize your AsciiDoc files any way you like by creating subdirectories and so on.
For example, the user guide might have subdirectories for each chapter.
|
The pages/ directory can have a a sibling images/ directory to hold the image files you reference in the user-guide.
Similarly there might be other sibling directories (specifically examples/, partials/, and attachments/) for other types of content in the module.
The names pages/, images/ etc. are specified by Antora.
They are the Antora family directories.
Adhering to this module structure isn’t a huge constraint.
While we have introduced a little more depth by using the pages/ subdirectory and so on, the required Antora naming scheme is fortunately not at all offensive or exceptional.
Even if we move away from Antora as our website builder it shouldn’t be hard to map this particular module organization onto whatever is required by the replacement.
|
That final nav.adoc file will consist of AsciiDoc lists and sub-lists that tell Antora how to navigate the content in this module.
While building the website Antora will use the nav.adoc file as a template for the sidebar the reader will use to navigate from.
The navigation file is a bit more specific to Antora but it is usually really small and there are ways to make it be of some general use.
The navigation file can be called anything (the nav.adoc name is not specified by Antora).
Also it need not sit at the root of the module though it generally makes sense to keep it there where the authors can quickly modify it as needed when they add or remove sections from the module.
|
Antora Components
An Antora component is just a package of modules. It can be any directory that contains two required elements:
|
A subdirectory with one or more Antora documentation modules laid out as shown above. |
||||||||||
|
A file that sets some overall configuration/metadata parameters for the Antora component.
The name is also mandatory.
|
| The component directory is not just restricted to the two required elements and can contain other files and subdirectories. |
An Antora component stores all the documentation modules that are naturally versioned, tagged, released, etc., together.
For example, at any given time the bulk of our FabulousApp clients will be using the current stable release of the software and therefore should see the corresponding stable release version of the install manual and user guide. A smaller group of adventurous users might be using the new beta version of the app and they need to see the beta install manual and user guide.
This is precisely a scenario that Antora is designed to deal with. It is the reason it requires that everything should be stored in a git repo in the first place. This is consistent with the idea that we should treat the documentation just like we treat the code base.
Just like FabulousApp, a repo may have multiple documentation modules. However, it will typically only have a single component so everything in the repo (the source code, the whole documentation tree etc.) is versioned at the same time.
By the way, the component directory name can be anything.
In an earlier version of this document we actually just plopped all the component requirements straight into the docs/ directory.
That works but things got a bit messy — experience shows that it is preferable to create some other directory for the component and other Antora artifacts.
|
The component idea is Antora specific so we will create it in an antora/ directory at the top level of our repo.
Here is the relevant section of the FabulousApp repo with the addition of the antora/ top level directory
FabulousApp ├── ... ├── antora (1) │ ├── antora.yml (2) │ └── modules -> ../docs (3) ├── docs │ ├── install-manual/... │ └── user-guide/... ├── ...
| 1 | This is the new directory for the Antora component which can be called anything you like. |
| 2 | The component has a required metadata file that must be called antora.yml. |
| 3 | The component has a required subdirectory that must be called modules with all the documentation content for the website in module form.
Here it is just a symbolic link to the docs/ directory at the top of the repo. |
Antora can follow symlinks.
This means that the component’s modules/ subdirectory can just be a symbolic link to a more conveniently located documentation tree.
So the docs/ directory doesn’t have to introduce another level or get renamed to modules which seems a little too Antora specific.
|
This is just one possible layout that will keep Antora happy. Lots of variations are possible. As their own documentation site puts it:
Antora employs both convention and configuration to aggregate content and generate your site.
You can see all the gory details about Antora conventions in the Antora documentation.
The Antora UI Bundle
Before we talk more about the process of building the site we should consider what we want the finished product to look like.
Like every other reasonable static website generator, Antora separates the look and feel (the user interface/UI) of the site from its content.
As outlined above, the content is stored in modules that are packaged in one or more components. It is written using the AsciiDoc markup format and stored in files and directories that follow Antora conventions. Each component is stored somewhere in a git repository either locally or out in the cloud, for instance on GitHub, GitLab, BitBucket, etc.
The UI is defined by a separate set of directories and files that are completely independent of that content. The UI holds all the graphical assets, styling directives and so on, that define the look and feel for the documentation website. To be used by Antora the whole UI directory is actually zipped up into what it calls a UI bundle.
The UI bundle is independent of the content and potentially can be reused across multiple sites. In practice, there will be some small changes needed to customize a generic UI for a specific site (perhaps change a logo or the names in a top-level menu dropdown and so on). Antora allows for this by incorporating the concept of a supplemental UI which is a small directory of extra files that get overlaid on the UI bundle. Files in the supplemental UI over-write any matching ones in the main UI bundle. We will see an example below.
Antora’s documentation for the Antora UI and the Antora UI bundle unfortunately is a bit sparse. There is a default UI in GitLab that you can study in all its glory and that does have some documentation.
The default UI has an equivalent UI bundle that you can use in your own documentation website. The good news is it will give you a very decent looking website with nice typography and a modern clean look and feel like this example.
| Even if you use the “default” UI bundle you will still need to supply some supplemental UI files to get rid of artifacts that will not make sense for your project. |
The Antora Playbook
We have seen that Antora honors the natural division of documentation into modules (the install manual, the user guide and so on).
It packages those modules into a component which is just a directory containing the modules and a little metadata configuration file antora.yml.
The component will probably live in the same repo as the associated source code and get updated and versioned in sync with that code base.
Larger projects will have lots of repos for different libraries and applications that work together as a system. In typical usage each of those will have their own Antora component to hold the associated documentation.
We need to tell Antora how to configure and build the actual documentation website from the content stored in one or more Antora components. What’s the title of the site, what’s its URL, what’s its landing page, what’s its look and feel, where is it published etc.
All of this is the role of the oddly named Antora playbook. To quote from the documentation an Antora playbook has several purposes:
-
It specifies the global information for the site such as its title and URL.
-
It specifies where to find all the content used in the site.
-
It specifies the home page for the site.
-
It specifies any AsciiDoc attributes or extensions that should be used when processing the content files. (For example, Antora does not support site search out of the box but you can call on an extension to automatically add that feature).
-
It specifies the UI bundle and any supplemental UI files that controls the visual layout, style, and behavior of the website. The playbook crucially is the glue between the content packaged in Antora components and the separate Antora UI bundle.
-
It specifies where and in what format the website should be published. That can be to a local directory or zip archive. It can even be directly to some custom provider that you code up using JavaScript.
-
It specifies any caching used by Antora and how to handle repository updates etc.
Running antora with the a playbook argument pulls in all the documentation components from the various repos the playbook references.
Antora then creates a virtual catalog of all those pages and assets giving each one an Antora ID that takes into account any version information, the name of the owning component and module, as well as the page itself of course. These ID’s are used to generate page URL’s and can also be used also for internal links and cross references. The repository structure for the documentation is never exposed.
In short a playbook.yml file sets the parameters Antora needs to produce a finished website.
The file itself can be called anything and it is the key argument for the antora command:
$ antora playbook.yml
Assuming everything goes well that command builds the documentation site.
Building the Site
So where should the playbook be stored and where will it put all the tools and artifacts it uses? Where will it store the actual output website?
Antora suggests that you set up a completely separate repo for this purpose.
In our case then you might create a different repo (say FabulousApp-antora) to house the playbook and any tools Antora needs to build FabulousApp’s documentation website.
Why would you introduce this complication?
Our app is simple enough to be stored in a single repo.
In a more complicated scenario we might have a whole system with several repos full of the source code for different libraries and applications that work together in some fashion.
Each library and application will have its own documentation in some Antora component which is just a directory somewhere in the coresponding repo with an antora.yml configuration file and a modules/ subdirectory in it.
Just like above, each module will in turn have its content in a pages/ subdirectory possibly supported by items in other Antora family directories.
In that scenario it’s not clear which of the repos should have the playbook that builds out the entire documentation website. Each repo owning its own documentation component is fine but it seems sensible to keep the master playbook in some separate location — the playbook-repo.
If you go this route the playbook-repo has none of the website source content. Instead it just has all the parameters and tools that make it possible to build the site and pulls in content as needed from the other repos. Any supplemental UI files will also be housed there. In large documentation projects the playbook repo probably isn’t really in the technical writer’s domain but more part of the developer world where they work on the website UI code and the website deployment scripts.
For a small single repo project like ours this is an overkill and there is no reason that the site cannot be built in the same repo that holds the source and documentation content.
So taking our FabulousApp example we might get to a repo structure like the following:
FabulousApp ├── ... ├── antora │ ├── antora.yml │ ├── modules -> ../docs │ ├── playbook.yml (1) │ ├── UI │ │ └── supplemental-ui/... (2) │ ├── build/... (3) │ └── ... (4) ├── docs │ ├── install-manual/... │ └── user-guide/... ├── src/... ├── README.adoc └── ...
| 1 | The configuration file for building the site is this playbook.yml file though the name can be anything. |
| 2 | Here we are using Antora’s default UI but need to override a few items by using the supplemental-ui feature. |
| 3 | We have added a new directory build which is where the site gets built locally.
This is probably not part of the git repository as it created on the fly |
| 4 | In practice there will be some other files and directories that Antora will use to cache dependencies from run to run. |
A very basic playbook.yml file for FabulousApp might look something like the following:
site: (1)
title: Our Fabulous App
start_page: fabulous-app:user-guide:introduction.adoc
content:
sources:
- url: .. (2)
start_path: antora (3)
ui: (4)
bundle:
url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable
supplemental_files: ./UI/supplemental-ui
output: (5)
dir: ./build
| 1 | This sets the website’s title and landing page which is a page in a module stored in a component. |
| 2 | This tells Antora where to put content from.
Each url should be a git repository and the start_path key tells Antora where to look for a component from the root of the repo. |
| 3 | In this example we are using Antora’s default UI though in practice we will need to override a few things.
The UI/supplemental-ui/ directory will have the necessary overrides. |
| 4 | This tells Antora to put the finished website into a local subdirectory called build/. |
| You probably will want to add some things to this playbook — for one thing it is a good idea to tell Antora to do some caching so everything needn’t get rebuilt for every small content change. You also might need to add some extensions that do things like automatically create a search index for your site etc. |
You invoke Antora as follows:
$ cd FabulousApp/antora
$ antora playbook.yml
If everything goes well you will get output along the following lines:
Site generation complete!
Open file:///.../antora/build/index.html in a browser to view your site.
Our Project
Introduction
The GF2++ project is stored in a git repository the following structure:
GF2 ├── .git/... ├── README.adoc ├── LICENSE ├── CMakeLists.txt (1) ├── include │ └── GF2 (2) │ ├── GF2.h │ ├── Matrix.h │ ├── Vector.h │ ├── Matrix.h │ ├── assert.h │ └── ... ├── examples/... (3) ├── docs (4) │ ├── reference-manual/... │ ├── documentation-strategy/... │ └── technical-notes/... └── ...
| 1 | All the library example and test programs are built using CMake which is the defacto standard for C++ projects. |
| 2 | This is a header-only library and the principal source code is in the include/GF2/ directory. |
| 3 | We provide lots of example programs that exercise various features of the library. |
| 4 | There are three documentation modules provided for the library each in its own subdirectory as shown. |
Before we talk in more detail about the documentation we should mention that the library really has just two substantial headers Vector.h and Matrix.h.
That assert.h is a short helper file defining macros used do optional assertions such as bounds checking primarily for debug builds.
The library ha some headers than this but for the purposes of this discussion we will stick to those.
There is a convenience header GF2.h that just includes all the others.
So, assuming your include path allows it, you use the library by just adding one line include <GF2/GF2.h> to the top of your program.
Documentation Layout
As you can see there are three separate modules in the docs/ directory:
-
You are reading content from the
documentation-strategy. -
The
technical-notesmodule is definitely for “extra credit” as it contains some mathematical papers that go into the gory underpinnings of the library and its algorithms. -
Finally we have
reference-manualwhich is by far the largest and most important module.
The /docs/reference-manual/pages/ directory closely mimics the layout of the library source code in the /include/GF2/ directory.
That pages/ directory looks somethings like:
docs/reference-manual
├── nav.adoc
└── pages
├── assert
│ ├── assert.adoc
├── Matrix
│ ├── Matrix.adoc
│ ├── access.adoc
│ ├── ...
│ └── triangle.adoc
├── Vector
│ ├── Vector.adoc
│ ├── access.adoc
│ ├── ...
│ └── to_string.adoc
├── assert
│ └── assert.adoc
└── overview.adoc
The documentation is in the form of AsciiDoc files stored under the pages/ subdirectory.
This layout makes it possible to easily use the static website builder Antora.
The pages/overview.adoc file introduces the library as a whole (this corresponds to the usual index.html for a website).
Then each class and macro in GF2++ has a corresponding documentation sub-directory in pages/.
For example, the fairly trivial <GF2/assert.h> header just defines the three related assert style macros and those are all documented in the one assert.adoc file.
In contrast, the GF2::Vector and GF2::Matrix classes have a large number of associated methods and quite a few documentation files.
Rather than having one documentation file per method we instead group methods and functions together where it logically makes sense for them to share a single file.
For example, the Vector/access.adoc file documents both GF2::Vector::operator()(i) methods, both GF2::Vector::operator[](i) methods, as well as the GF2::Vector::test(i) method.
These are grouped together because each provides access to the individual element at a given index i in a bit-vector.
The signature for each of the five is explained, how bounds checking can be enabled for the index, and the access.adoc file includes a short sample code that exercises the methods and the output from that code is shown.
The user should be able to copy-paste that snippet, compile, and reproduce the shown results.
The main entry point for documenting the GF2::Vector class is the file Vector/Vector.adoc and for the GF2::Matrix class it is Matrix/Matrix.adoc.
Those two files lay out the overall rationale behind the classes as well as providing links to all the other documentation in a nice table format.
The whole effect is quite similar to what you get if say you look up std::vector at CPP Reference/vector.
Repository Structure
Expanding a little on our earlier picture the GF2++ repo now looks something like:
GF2 ├── .git/... ├── CMakeLists.txt ├── README.adoc ├── antora/... (1) ├── include │ └── GF2 (2) │ ├── GF2.h │ ├── Matrix.h │ ├── Vector.h │ ├── Matrix.h │ ├── assert.h │ └── ... ├── examples/... ├── docs (3) │ ├── documentation-strategy/... │ ├── reference-manual │ │ ├── nav.adoc │ │ └── pages (4) │ │ ├── Matrix │ │ │ ├── Matrix.adoc (5) │ │ │ ├── access.adoc │ │ │ ├── ... │ │ │ └── triangle.adoc │ │ ├── Vector │ │ │ ├── Vector.adoc (6) │ │ │ ├── access.adoc │ │ │ ├── ... │ │ │ └── to_string.adoc │ │ ├── assert │ │ │ └── assert.adoc (7) │ │ └── overview.adoc (8) │ └── technical-notes/... │ └── common.adoc (9) └── ...
| 1 | We’ll discuss the antora/ directory in the next section below. |
| 2 | The source code for this header-only library is all in include/GF2/. |
| 3 | The docs/ subdirectory has the documentation content in three modules discussed above. |
| 4 | Inside a module the content is primarily stored in its pages/ subdirectory (there might be an images/ subdirectory also.). |
| 5 | The Matrix.adoc file has the overview of the GF2::Matrix class.
The many other files in this directory document groups of related class methods and functions. |
| 6 | There is a similar structure for the GF2::Vector class. |
| 7 | There is a similar structure for the gf2_assert macros. |
| 8 | The reference-manual/pages/overview.adoc file introduces the library as a whole. |
| 9 | This common.adoc file has some AsciiDoc attributes/settings for all the documentation.
Most of the .adoc files in the tree include this at the top. |
We make use of common.adoc to set a few attributes for most of our AsciiDoc files.
These include the copyright notice and a couple of items that make Antora and a simpler AsciiDoc previewer work nicely together.We usually write content in an editor that autosaves and simultaneously preview just that one document in a browser with an AsciiDoc plugin installed. This is very fast but the browser based plugin is not as sophisticated as Antora so needs a bit of help to understand what those Antora family directories can be found. |
Antora
The top level docs/ directory is organized in a natural manner though we have made an effort to keep it compatible with the way Antora would like things.
In particular, we have split the documentation into modules and then under each module stored the actual AsciiDoc content under a pages/ subdirectory, images under an images/ subdirectory and so on.
As we mentioned earlier, adhering to this structure isn’t really much of a constraint.
Even if we move away from Antora as our website builder it shouldn’t be hard to map this particular docs/ organization in to whatever is required by the replacement.
|
It does seem very sensible to keep the Antora specific parameter files, build instructions, needed support tools etc. in their own sandbox.
In fact, the Antora documentation suggests having a completely separate repo for all that stuff.
For a small library like ours that seems a bit of an overkill so instead we settled for having a dedicated antora/ directory at the top of the repo which looks like:
GF2/antora ├── antora.yml (1) ├── modules -> ../docs (2) ├── UI (3) │ └── supplemental-ui/... ├── antora-playbook.yml (4) └── antora-playbook-local.yml (5) ├── build/... (6) ├── Makefile (7) ├── package-lock.json ├── package.json (8) ├── README.adoc (9) ├── netlify/... (10)
| 1 | This GF2/antora/ directory is an Antora component so has an antora.yml file and a modules/ subdirectory. |
| 2 | The modules/ subdirectory is really just a symbolic link to the actual documentation tree at the top of the repo.
Antora handles symlinks just fine. |
| 3 | The UI/ subdirectory has all the assets that define the look and feel of the website.
In our case we are using the default UI to just need to override a few items in UI/supplemental-ui to get that to work. |
| 4 | The playbook tells Antora how to build the website.
This master antora-playbook.yml file tells Antora to pull the documentation from the remote repo which in our case is hosted on GitLab. |
| 5 | This antora-playbook-local.yml version instructs Antora to use the content from the local machine which is handy when you are developing the documentation.
It is actually created on the fly from the master playbook so does not need to be checked in to git. |
| 6 | No matter which playbook you use Antora will be told to build the website in this local build/ subdirectory. |
| 7 | The Makefile can be used to invoke Antora correctly (so make local to invoke it on the local playbook and make remote to invoke it on the remote playbook).
It can also be used to keep the two playbooks in sync. |
| 8 | We pull in Antora itself and any needed extensions using npm which is configured in package.json. |
| 9 | There is a README file to explain all of this. |
| 10 | The production version of the Antora built site is hosted by netlify. There is a job set up on netlify to invoke Antora automatically and deploy the site — various configuration parameters for that job are in this subdirectory. |
runtime: (1)
cache_dir: ./.cache/antora
log:
failure_level: warn
site: (2)
title: The GF2++ Class Library
start_page: gf2pp:reference-manual:overview.adoc
robots: allow
keys:
google_analytics: 'G-KMH2PCJ214' (3)
content: (4)
edit_url: false
sources:
- url: https://gitlab.com/nzzn/gf2.git
start_path: antora
asciidoc: (5)
attributes:
experimental:
ui: (6)
bundle:
url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable
snapshot: true
supplemental_files: ./UI/supplemental-ui
antora: (7)
extensions:
- "@antora/lunr-extension"
output: (8)
dir: ./build
| 1 | Set up the overall level of logging and get Antora to cache things between builds. |
| 2 | Set the documentation site’s title and the landing page. Happy to have the site crawled by search engines. |
| 3 | Antora can embed keys/tags in the website pages (see how we use this one below). |
| 4 | Repo to pull the documentation component from and its location in that repo. |
| 5 | Antora can pass along attributes to AsciiDoc. |
| 6 | We are using the default UI and overrode a few things to personalize that. |
| 7 | We use lunr to build a search index for the site & use the Antora lunr extension to invoke it. |
| 8 | Tell Antora to put the built site into this directory. |
| This project’s docs are deployed on the free version of netlify which doesn’t give feedback on how busy the site is. However, we can use Google Analytics to get at that data. For obvious reasons Google Analytics requires that you prove a site is yours before they hand over any data about its traffic. They do that by asking you to embed some personalized tags in all the pages. If you give Antora the appropriate key which is public anyway it can do that job for you at build time. |
Gotchas
Overview
Not everything is plain sailing with either AsciiDoc or Antora. On the plus side the combination is powerful enough to tackle large scale documentation projects. However, they aren’t the easiest set of tools to learn and can seem complicated for smaller scale problems.
Antora also extends AsciiDoc by introducing the family directory concept. This is a key Antora idea that is particularly important for large multi-repo projects. However, perhaps more thought should have been given to making it backward compatible with plain standard AsciiDoc — at least for simpler projects.
AsciiDoc is Wordy
Writing basic AsciiDoc is really no different from writing Markdown but of course that isn’t really a testament to its usability.
You are only going to write AsciiDoc to access the extra features it unlocks. The essential cost benefit comparison is then whether those “extras” come at a low enough cost to justify the effort of learning a new syntax.
Certain features AsciiDoc provides out of the box are without doubt really useful for writing technical documentation and very easy to use! In particular, callouts and admonitions are enormously useful, much used, and take very little effort to learn.
Other features of AsciiDoc are occasionally useful but you will probably find yourself needing to go to the manual to refresh your memory on how to invoke them. This is because AsciiDoc syntax can be a bit wordy and frankly some aspects simply aren’t explained all that well in the core documentation so it is hard to develop an intuition how to do things.
One specific example is math mode in AsciiDoc. Technical documentation will often need to embed mathematical equations both inline and display style. The good news is that you can readily get AsciiDoc to support \(\TeX\) style equations but unfortunately in a somewhat wordy manner.
Using \(\TeX\) you write $A \cdot x$ to get the inline equation \(A \cdot x = b\).
In AsciiDoc (certainly if you want to use Antora) you instead need to write the longer stem:[A \cdot x = b].
It is even more wordy and space consuming to write the display version:
[stem]
++++
A \cdot x = b
++++
This does display correctly as
but the raw source you are editing is much more visually cluttered than the equivalent \(\TeX\) code:
$$ A \cdot x = b $$
One other thing that is less of a criticism but something to bear in mind is that at the end of the day, AsciiDoc is just a markup language for documents. As such it lies in complexity and capability between its simpler cousin Markdown and something like \(\LaTeX\) which is heavily used in many scientific disciplines.
You can write some very simple macros in AsciiDoc but those are mostly for doing substitutions. So you can assign an short attribute to say a long messy URL and then refer to that URL by using the attribute. This is definitely useful but a long way short of the type of things you can do (granted often painfully) in \(\TeX\).
And of course while AsciiDoc allows you to link documents together in a cohesive manner, it is not by itself by any means a website generator. You use the language to write the content for a website but bear in mind that you will need to go elsewhere to actually get a reasonable looking website.
Antora Extends AsciiDoc
A key improvement of AsciiDoc over Markdown is that it allows you to include one file (or even just a tagged portion of a file) in another one.
The target file in an AsciiDoc include directive is usually specified by simply giving its location relative to where you doing the actual include::.
For example to include a source code file main.cpp that lives a couple of levels up in the source/ directory you might write:
include::../../source/main.cpp[]
AsciiDoc does support some variations on this theme but they are all equally straightforward.
The same sort of simple target resolution holds for inter-document cross references (an AsciiDoc xref) and for locating image files (the AsciiDoc image directive).
The downside is that this can be fragile especially if you want to reference files that exist in different repositories. AsciiDoc will let you do that but only by hard coding the URL of the target repo and the location of target file from there. If anything changes you end up with a lot of URLs to fix in your AsciiDoc files.
Antora is specifically targeted at large documentation projects where the components are spread across many repositories so this is a key problem it aims to fix.
It does so by applying a core notion. In Antora that all resources that go into the website (pages, images, snippets/partials, downloadable attachments etc.) are given a unique identifier—an Antora ID.
During the build process Antora gathers all of the website resources into a virtual filesystem and assigns one of those ID’s to each item.
Antora more or less insists that you use those ID’s when you want to include a resource either through the include, xref, or image directives.
In the greatest generality you do that by using all of the target resource’s Antora coordinates.
These coordinates are exhaustive but also human friendly and include a version tag, a component name, a component module, an Antora family, and finally the name of the resource itself.
These Antora coordinates are independent of the physical layout of the resource files and so eliminate the need for those somewhat hacky ../.. references in your documentation files.
That’s great and even better is the fact that most of the time you don’t need to specify almost any of those Antora coordinate elements. A directive like:
image::SomeImageFile.png[]
tells Antora to find SomeImageFile.png in the appropriate family directory which is always called images/ and always is a sibling to pages/ (in fact all the recognized family directories are siblings of pages/).
The Problem with Families!
AsciiDoc itself knows nothing about these Antora family directories.
Neither do any of the Asciidoctor based renderers.
The family directory concept is an Antora extension to standard AsciiDoc.
Antora’s core development team is undoubtedly capable and very productive, but it is also very small. Antora might die tomorrow or some alternative “better” website generator might come along that uses AsciiDoc as the content format. Locking oneself to AsciiDoc as a markup format seems fine, locking oneself to a very specific Antora content layout and relying on the family directory structure doesn’t seem so great.
Even on a short term basis, you will hit a problem when you try to preview individual AsciiDoc files in your browser/editor.
The JavaScript AsciiDoc renderer in that plug-in knows nothing about the Antora family directories.
That simple image:: directive above will fail because the renderer will look in the current directory for the
SomeImageFile.png file instead of where it actually sits in the ../images/ directory.
Of course, you could always insist on using Antora before previewing a document. However, that entails building out the whole website when you just want a quick peek at one page!
| Antora makes complicated things possible but isn’t great at making simple things simple. If you want your content files to also work outside the context of Antora you need to work at it a bit! |
For what it’s worth, the Antora team is working on a tool that will take markup that uses those Antora coordinates and emit standard AsciiDoc markup where the references are appropriately resolved. That may help a little but still locks the writer into using the family directories which is an Antora extension.
Workarounds
For a simple site like ours with one component there is a pretty easy work-around.
The standard for AsciiDoc renderers is to always look in the :imagesdir: for target image files if that attribute happens to be set.
On the other hand, Antora completely ignores the :imagesdir: attribute and only ever looks in the images$ “family” directory.
We can keep all systems happy by using the Antora family directory for images and then setting imagesdir at the start of the document to the actual relative location of that directory—something along the lines of:
:imagesdir: ../images
Then when you quickly preview the file in your editor or browser the target of the image:: directive will be satisfactorily resolved.
More generally, you can check whether the site-gen-antora attribute is set to add or remove things depending on whether you are using Antora or some other AsciiDoc processor.
For example, it is often handy to have a line like ifndef::site-gen-antora[:toc: left] at the start of a document.
If you are previewing the file outside of Antora then you get a useful clickable table of contents on the left for quick navigation in larger documents.
That table of contents is usually redundant in Antora as in the full website some other more sophisticated navigation is in play.
|
Getting Help
AsciiDoc is documented on the AsciiDoc documentation website, and Antora on the Antora documentation site.
AsciiDoc has been around a while so you can find other sources of tips by Googling. Though it’s not been added to in a while, I found the Awesome Asciidoctor site to be useful (the site’s content is also available as a book).
Antora is newer so there isn’t that much readable material available beyond the official documentation site.
There is an active forum you can follow on Zulip. However, the Antora Zulip site doesn’t appear to be indexed by the standard search engines which is annoying! If you type an Antora related search term into Google you will not see any results from the forum (though you may pick something up from an earlier one that existed prior to the migration to Zulip). A new user is expected to find Zulip through the Antora docs and then use the internal search tool there. This adds another point of friction when you are learning the system.