Skip to content

hybras/asciidoctor-server

Repository files navigation

Asciidoctor Server

This is an attempt to have asciidoctor run as a server. This is useful for programs that use asciidoctor’s cli, like static site generators (that aren’t written in ruby). Spawning a ruby process for every file is very wasteful and squanders asciidoctor’s speed.

Warning
THIS IS ALPHA SOFTWARE, USE AT YOUR OWN RISK.

Installation

Tip
Use direnv (or many similar tools) to create an environment for your website. Install the client within this environment.

Make sure the client and server version agree. Check the list of git tags. We are at semver 0 and so regularly make breaking changes.

Client

The client is availible as a cargo package and as a nix package

Cargo

cargo install asciidoctor-client

Nix
{
  description = "A Nix Environment for running asciidoctor-client";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05";
  inputs.flake-utils.url = "github:numtide/flake-utils";
  inputs.asciidoctor-server = {
    url = "github:hybras/asciidoctor-server";
    inputs.nixpkgs.follows = "nixpkgs";
    inputs.flake-utils.follows = "flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils, asciidoctor-server }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = import nixpkgs { inherit system; }; in
        {
          devShells = {
            default = pkgs.mkShell {
              buildInputs = [
                asciidoctor-server.packages.${system}.asciidoctor-client
              ];
            };
          };
        });
}

Server

Use one of the following options. Feel free to also add specific versions of asciidoctor and its extensions / optional dependencies. The server will load them on request.

  • bundle add asciidoctor-server

  • Add the following to your Gemfile

    gem "asciidoctor-server", "$VERSION"
  • gem install asciidoctor-server

Shim Script

Add the following shim to your path under the name asciidoctor

Note
I hard code the address of the server, but feel free to leave it as an environment variable.
asciidoctor
#!/bin/bash

exec asciidoctor-client --address $ADDRESS "${@:1:$#-1}" yeet

Usage

Warning
I haven’t yet setup an easy way to stop/start the server or have the client discover the server. Also there’s no security so don’t use in prod.
Caution
Extensions cannot be loaded / unloaded on demand. Once loaded, they are enabled for all subsequent requests. This is not a problem for hugo, which passes the same cli args (ie, extensions) on every invocation. It might be a problem for other setups

Pick an address for the server to bind to, maybe something like $PROJECT_ROOT/.asciidoctor-server.sock

  1. Start the server with bundle exec, ie bundle exec asciidoctor-server -addr $ADDRESS. You’ll probably need to use job control or a separate shell to have this run in the background.

  2. Build your website (that’s hugo for me). If your shim was setup correctly, your static site generator will use it. You can test this by adding print statement to the shim script.

Implementation

This project was written with the constraint that hugo and asciidoctor should run unmodified.

The client and server communicate using grpc, with the client mapping its cli arguments to the asciidoctor api. Only a small subset of asciidoctor’s arguments are supported.

Normally, hugo spawns a converter process per file. The program that is spawned corresponds to the content format. Markdown support is built into hugo, so it spawns a goroutine for markdown files.

Asciidoctor is written in ruby, and is a very large program with many optional features. While its conversion speed is excellent, its startup time is not.

This project consists of a lightweight client (there are go and rust clients in tree, though it could have been any compiled language). The client starts quickly and makes a grpc request to the server. We only pay the startup cost once.

Unfortunately, this makes the conversion process significantly more complicated. There is now a server and client sitting between hugo and asciidoctor. Both are poorly written, exposing only a fraction of asciidoctor’s functionality. If unsupported args are passed, the client should error.

Development

You’ll need the protobuf compiler, protoc, version 3. This is included in the nix flake for this repo.

Client

  1. Install the go protobuf compiler plugins

    go install google.golang.org/protobuf/cmd/[email protected]
    $ go install google.golang.org/grpc/cmd/[email protected]
  2. Update your PATH so that the protoc compiler can find the plugins. This is not necessary if you’re using direnv.

    export PATH="$PATH:$(go env GOPATH)/bin"

Server

Do the following under server

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to [rubygems.org](https://rubygems.org).

Future

Given how hacky this is, this is not a long term solution. Long term solutions include:

  • an implementation of asciidoc with a shorter startup time (perhaps in a compiled language?)

    • If a go implementation existed, it could be included in hugo. The author has expressed support for this idea given a suitable go library.

    • Asciidoctor’s startup time might improve, but this is a difficult undertaking

  • The basic principle of this (a single process / goroutine that does all conversions, and communication occurs over message passing) is merged into hugo. This is far more feasible than the other options, but would require a rearchitecture of how hugo handles external converters. It wouldn’t make sense to do this solely for asciidoc, unfortunately.