Use the API
Instead of relying on the built-in initialization activated by an exporter extension, you can use the Assembler API directly to generate exports, giving you full control over the configuration.
Entry points
To use the programmatic configuration, you’ll need to create an Antora extension that acts as an entry point for Assembler. You can use one of two APIs provided by Assembler:
-
configure(context, converter, config, providers)(typically mapped toconfigureAssembler) -
assembleContent(playbook, contentCatalog, converter, providers)
The simplest approach is to the use the configure function since it takes care of registering the correct event listeners.
In this scenario, your extension has to do the following:
-
Define (or load) a converter
-
Define a configuration object (so Assembler doesn’t look for a configuration file)
-
Optionally define a navigation catalog to control the navigation tree Assembler uses
-
Invoke the
configurefunction, passing the generator context (i.e.,thisof an extension event listener), converter, config, and providers
The config parameter accepts a configSource object to override the Assembler configuration.
A more low-level approach is to use the assembleContent function.
When using this function, your extension must handle the work of registering the event listeners that the configure function normally does.
In this scenario, your extension has to do the following:
-
Configure Antora’s AsciiDoc loader to keep the AsciiDoc source
-
Define (or load) a converter
-
Define a configuration object (so Assembler doesn’t look for a configuration file)
-
Optionally define a navigation catalog to control the navigation tree Assembler uses
-
Invoke the
assembleContentfunction bound to the generator context (i.e.,thisof an extension event listener)
When using the assembleContent function directly, you lose the profile-based navigation feature (unless you populate the assemblerProfiles context variable or faithfully implement the same logic from the configure function).
That’s why we always recommend using the configure function if you can.
The providers parameter for both functions accepts overrides for built-in functionality:
-
For
configure,providersaccepts{ assembleContent, navigationCatalog, componentVersionFilter }. -
For
assembleContent,providersaccepts{ configSource, navigationCatalog, componentVersionFilter }.
The assembleContent function overrides the built-in function.
The navigationCatalog object overrides the navigation resolver.
The componentVersionFilter function overrides the built-in component version filter logic for the array of components from the content catalog, which it accepts as its only parameter.
Note that the configSource is passed through the providers parameter for the assembleContent function, whereas it is passed through the config parameter for the configure function.
Let’s look at how to use each function in detail through the lens of some examples.
Usage
configure
In this example, we’re going to create an Antora extension that leverages Assembler using the configure API.
This extension configures Assembler to generate one PDF export for every publishable page in the latest version of the component named component-name.
It does so by creating a custom navigation that has one top-level item per publishable page and setting the root level to 1.
module.exports.register = function () {
const configSource = {
componentVersionFilter: {
names: ['component-name'],
},
rootLevel: 1,
}
const navigationCatalog = {
getNavigation: function (component, version) {
return [
{
items: this.getVariables()
.contentCatalog.findBy({ component, version, family: 'page' })
.filter((it) => it.out)
.map((it) => ({
content: it.asciidoc.navtitle,
url: it.pub.url,
urlType: 'internal',
})),
},
]
}.bind(this),
}
const { configure: configureAssembler } = require('@antora/assembler')
const converter = require('@antora/pdf-extension/converter')
configureAssembler(this, converter, { configSource }, { navigationCatalog })
}
The trickiest part is defining the custom navigation catalog.
It must be an object with a single method named getNavigation that returns a navigation tree for the specified component version.
Since the content catalog isn’t yet available when the extension is registered, you have to bind the generator context to the getNavigation function so it can look up the content catalog on demand.
Each entry in the navigation tree must define a content key, which is the visible text.
It may also have a url key that’s either an internal (pub) URL in the Antora site or an external URL.
An internal URL, which must be indicated using the urlType key, is a root-relative path to the resource in the site.
For pages, you’ll likely want to use the content catalog to retrieve this information, as shown in the example.
A copy of the configSource object will still be populated with default values by Assembler.
The original object is not modified.
|
assembleContent
Let’s look at the previous example again, except this time using the assembleContent function instead of the configure function.
module.exports.register = function () {
const configSource = {
componentVersionFilter: {
names: ['component-name'],
},
rootLevel: 1,
}
const navigationCatalog = {
getNavigation: function (component, version) {
return [
{
items: this.getVariables()
.contentCatalog.findBy({ component, version, family: 'page' })
.filter((it) => it.out)
.map((it) => ({
content: it.asciidoc.navtitle,
url: it.pub.url,
urlType: 'internal',
})),
},
]
}.bind(this),
}
const { configure: configureAssembler } = require('@antora/assembler')
const converter = require('@antora/pdf-extension/converter')
configureAssembler(this, converter, { configSource }, { navigationCatalog })
}
Notice that the extension now has to configure Antora to enable the export publishable family and keep the AsciiDoc source.
It also needs to add the navigationBuilt event listener in which to call the assembleContent function.
Unlike the configure function, you must bind the generator context to the assembleContent function when invoking it.
You also need to pass the playbook and content catalog as the first and second arguments, respectively.
You don’t have to bind the generator context to the getNavigation function since the content catalog is now available inside the event listener.
If possible, use the configure function.
If you need more low-level control, only then should you use the assembleContent function.
But keep in mind, it does create some limitations.
Dynamic profiles
There are two ways to add configuration for assembler profiles dynamically. One way is to add configuration for Assembler to the descriptor attached to the component version origin. The other way is to add an entry to the context variable where the assembler profiles are stored.
Descriptor
Let’s assume that we want to use conditionals in our navigation, but we don’t want to modify the content source root. We can use an extension to inject the necessary configuration.
Let’s first look at how to add configuration for Assembler to the primary descriptor.
module.exports.register = function () {
this.once('componentsRegistered', ({ contentCatalog }) => {
for (const component of contentCatalog.getComponents()) {
for (const componentVersion of component.versions) {
const primaryOrigin = findPrimaryOrigin(componentVersion)
if (!primaryOrigin || 'assembler' in (primaryOrigin.descriptor?.ext ?? {})) continue
const filesByPath = componentVersion.files.reduce((accum, it) => (accum[it.path] = it) && accum, {})
const usesConditional = componentVersion.nav.find(
(it) => it in filesByPath && filesByPath[it].contents.includes('def::loader-assembler[]\n')
)
if (usesConditional) {
;((primaryOrigin.descriptor ??= {}).ext ??= {}).assembler = { nav: componentVersion.nav }
}
}
}
})
}
function findPrimaryOrigin (componentVersion) {
let primaryOrigin = componentVersion.nav?.origin
if (primaryOrigin) return primaryOrigin
for (const origin of componentVersion.origins) {
if (primaryOrigin == null || origin.descriptor?.ext?.assembler) primaryOrigin = origin
}
return primaryOrigin
}
In this example, we’re manually adding the assembler key to the component version descriptor if a conditional is detected in one of the nav files.
That will force Assembler to reload those nav files and thus reevaluate any conditionals.
This extension must be registered before the Assembler extension.
| The concept of the primary origin is still a bit fuzzy in Antora and is subject to change. |
Context variable
Let’s look at how to do the same thing by updating the map of assembler profiles, which are stored in the assemblerProfiles context variable.
module.exports.register = function () {
this.once('contentClassified', ({ assemblerProfiles, contentCatalog }) => {
let updated
for (const { name: componentName, versions } of contentCatalog.getComponents()) {
for (const componentVersion of versions) {
const { nav: navs, version } = componentVersion
const componentVersionKey = `${version}@${componentName}`
if (assemblerProfiles.has(componentVersionKey)) continue
const navFiles = navs.reduce((accum, path) => {
const navFile = contentCatalog.getByPath({ component: componentName, version, path })
if (navFile) accum.push(navFile)
return accum
}, [])
if (navFiles.find((it) => it.contents.includes('def::loader-assembler[]\n'))) {
assemblerProfiles.set(componentVersionKey, new Map().set(undefined, { navFiles }))
updated = true
}
}
}
if (updated) this.updateVariables({ assemblerProfiles })
})
}
In this example, we’re explicitly adding a profile entry if a conditional is detected in a nav files. This extension can be registered before or after the Assembler extension.
The assemblerProfiles context variable is an experimental API, so you may have to update this code in a future release of Assembler.
|
Postprocessing
Export files are stored in the content catalog.
That means you can manipulate the out and pub properties of those files during or after the navigationBuilt event if you want them to be published to a different URL.
This works regardless of how you use Assembler, whether you configure it directly or run it by way of an exporter extension.