Class-Based Extension

If your extension is going to listen for multiple events and keep track of state, you may want to consider defining your extension as a JavaScript class. A class is a template for creating an object. It encapsulates both data and the methods that operate on that data. This encapsulation can help keep your extension more organized. The challenge is to work out how to define a class in such a way that it can be used as an Antora extension. This page shows you how.

Extension class structure

The basic structure of a class-based extension is as follows:

  • Define a class named after your extension (e.g., MyExtension)

  • Add a static register method that Antora can call

  • Define listeners as instance methods (e.g., onPlaybookBuilt ({ playbook }))

  • Add a constructor that accepts the generator context and registers listeners from the class

  • Export the class definition

Here’s the skeleton of our extension class:

class MyExtension {
}

Let’s fill in the details.

The register method and instantiation

Antora won’t create an instance of your class, but you can use the static register method on the class to do so. If you’re coming from Java, you can think of it like the main method of the class. Here’s how that entry point looks:

class MyExtension {
  static register () {
    new MyExtension(this)
  }
}

module.exports = MyExtension

All Antora will see is the register method on the exported class definition, which Antora will invoke to get the process started. The remainder of the work happens in the extension instance.

Notice how the static register method has transitioned us from a static function to an instance of the extension class. The register method passes the generator context to constructor of the extension class so it can access and store a reference to it.

Listener methods

Listeners are defined as methods on the extension class. They will get invoked just like any other listener function, only they will have a reference to both the current instance of the class (this) and the generator context (this.context). That way, they can access both properties on the extension (extension state) and context variables in the generator. Here’s the extension class again with the listeners defined as methods:

class MyExtension {
  static register () {
    new MyExtension(this)
  }

  onPlaybookBuilt () {
    this.startTime = +new Date
  }

  onSitePublished () {
    const elapsed = +new Date - this.startTime) / 1000
    const logger = this.context.require('@antora/logger')('my-extension')
    logger.info(`elapsed time: ${elapsed}s`)
  }
}

module.exports = MyExtension

Now all that’s left is to wire these listeners to events.

The constructor and adding listeners

The next step is to create a constructor the accepts the generator context and adds listeners. Here’s the extension class again with the constructor:

class MyExtension {
  static register () {
    new MyExtension(this)
  }

  constructor (generatorContext) {
    ;(this.context = generatorContext)
      .on('playbookBuilt', this.onPlaybookBuilt.bind(this))
      .on('sitePublished', this.onSitePublished.bind(this))
  }

  onPlaybookBuilt () {
    this.startTime = +new Date
  }

  onSitePublished () {
    const elapsed = +new Date - this.startTime) / 1000
    const logger = this.context.require('@antora/logger')('my-extension')
    logger.info(`elapsed time: ${elapsed}s`)
  }
}

module.exports = MyExtension

When adding each listener, it must be bound to the extension instance (i.e., this). Otherwise, the listeners won’t be able to access the properties on the extension instance. The listener can still access the generator context using the context property, to which the constructor assigns the generator context.

As you have seen, using a class-based extension can keep your extension code more organized. It also allows your extension to take advantages of other object-oriented patterns, such as inheritance, composition, and delegation.