@antora/assembler
Version:
A JavaScript library that merges AsciiDoc content from multiple pages in an Antora site into assembly files and delegates to an exporter to convert those files to another format, such as PDF.
146 lines (137 loc) • 6.58 kB
JavaScript
const createAsciiDocFile = require('./util/create-asciidoc-file')
const filterComponentVersions = require('./filter-component-versions')
const produceAssemblyFile = require('./produce-assembly-file')
const selectMutableAttributes = require('./select-mutable-attributes')
const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, resolveAssemblyModel) {
const { asciidoc: assemblerAsciiDocConfig, assembly: assemblyConfig } = assemblerConfig
resolveAssemblyModel ??= (componentVersion) => ({
doctype: assemblyConfig.doctype,
insertStartPage: assemblyConfig.insertStartPage,
rootLevel: assemblyConfig.rootLevel,
sectionMergeStrategy: assemblyConfig.sectionMergeStrategy,
navigation: componentVersion.navigation,
xmlIds: assemblyConfig.xmlIds,
})
const assemblerAsciiDocAttributes = Object.assign({}, assemblerAsciiDocConfig.attributes)
const { revdate, 'source-highlighter': sourceHighlighter } = assemblerAsciiDocAttributes
delete assemblerAsciiDocAttributes.revdate
delete assemblerAsciiDocAttributes['source-highlighter']
const publishableFiles = contentCatalog.getFiles().filter((file) => file.out)
return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersionFilter.names).reduce(
(accum, componentVersion) => {
const assemblyModel = resolveAssemblyModel(componentVersion)
if (!assemblyModel.navigation) return accum
const { name: componentName, version, title } = componentVersion
const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
attributes: Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
})
const mergedAsciiDocAttributes = mergedAsciiDocConfig.attributes
const auxiliaryImages = new Set()
Object.entries(mergedAsciiDocAttributes).forEach(([name, val]) => {
const match = name.endsWith('-image') && val.startsWith('image:') && IMAGE_MACRO_RX.exec(val)
if (!(match && isResourceRef(match[1]))) return
// Q should we allow image to be resolved relative to component version?
const image = contentCatalog.resolveResource(match[1], undefined, 'image', ['image'])
if (!image?.out) return
mergedAsciiDocAttributes[name] = `image:${image.out.path}[${match[2]}]`
auxiliaryImages.add(image)
})
const rootEntry = { content: title }
const { navigation, rootLevel } = assemblyModel
let startPage =
'startPage' in componentVersion
? componentVersion.startPage
: contentCatalog.resolvePage('index.adoc', { component: componentName, version })
if (startPage && startPage.src.component === componentName && startPage.src.version === version) {
if (assemblyModel.insertStartPage) {
const navtitle = startPage.asciidoc?.navtitle || rootEntry.content
Object.assign(rootEntry, { navtitle, url: startPage.pub.url, urlType: 'internal' })
}
} else {
// Q: should we always use a reference page as startPage for computing mutableAttributes?
startPage = createAsciiDocFile(contentCatalog, {
src: {
component: componentVersion.name,
version: componentVersion.version,
module: 'ROOT',
family: 'page',
relative: '.start-page.adoc',
origin: (componentVersion.origins || [])[0],
},
})
}
const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
delete mutableAttributes.doctype // Q: should this be in selectMutableAttributes?
prepareOutlines(navigation, rootEntry, rootLevel).reduce((any, outline) => {
const assemblyFile = produceAssemblyFile(
loadAsciiDoc,
contentCatalog,
componentVersion,
outline,
publishableFiles,
mergedAsciiDocConfig,
mutableAttributes,
assemblyModel
)
if (!assemblyFile) return any
if (!any && auxiliaryImages.size) {
const assembledAssets = assemblyFile.assembler.assembled.assets
auxiliaryImages.forEach((asset) => assembledAssets.add(asset))
}
accum.push(assemblyFile)
return true
}, false)
sourceHighlighter
? (mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter)
: delete mergedAsciiDocAttributes['source-highlighter']
return accum
},
[]
)
}
function getAsciiDocConfigWithAsciidoctorReducerExtension (componentVersion) {
const asciidoctorReducerExtension = require('@asciidoctor/reducer') // NOTE: must be required lazily
const asciidocConfig = componentVersion.asciidoc
const extensions = [asciidoctorReducerExtension]
const configuredExtensions = asciidocConfig.extensions || []
if (!configuredExtensions.length) return Object.assign({}, asciidocConfig, { extensions, sourcemap: true })
return Object.assign({}, asciidocConfig, {
extensions: configuredExtensions.reduce((accum, candidate) => {
if (candidate !== asciidoctorReducerExtension) accum.push(candidate)
return accum
}, extensions),
sourcemap: true,
})
}
function includedInNav (items, url) {
return items.find((it) => it.url === url || includedInNav(it.items || [], url))
}
function isResourceRef (target) {
return ~target.indexOf(':') && !(~target.indexOf('://') || (target.startsWith('data:') && ~target.indexOf(',')))
}
function prepareOutlines (navigation, rootEntry, rootLevel) {
const singleEntry = navigation.length === 1
const items = navigation.reduce((accum, it) => {
if (!it.content || (singleEntry && it.url ? it.url === rootEntry.url : it.content === rootEntry.content)) {
accum.push(...it.items)
} else {
accum.push(it)
}
return accum
}, [])
if (rootLevel === 0) {
if (rootEntry.url && includedInNav(items, rootEntry.url)) {
for (const p of ['url', 'urlType']) delete rootEntry[p]
}
return [Object.assign(rootEntry, { index: true, items })]
}
if (rootEntry.url && !includedInNav(items, rootEntry.url)) {
rootEntry.content = singleEntry && rootEntry.url === navigation[0].url ? navigation[0].content : rootEntry.navtitle
items.unshift(rootEntry)
}
return items
}
module.exports = produceAssemblyFiles