@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.
217 lines (206 loc) • 9.53 kB
JavaScript
const computeOut = require('./util/compute-out')
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 ATTR_REF_RX = /\\?\{(\w[\w-]*)\}/g
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,
embedReferenceStyle: assemblyConfig.embedReferenceStyle,
linkReferenceStyle: assemblyConfig.linkReferenceStyle,
dropExplicitXrefText: assemblyConfig.dropExplicitXrefText,
logger: assemblyConfig.logger, // for tests
})
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)
let siteRoot
const configMdc = assemblerConfig.file ? { file: { path: assemblerConfig.file } } : {}
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 mergedAsciiDocAttributes = collateAsciiDocAttributes(
Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes),
assemblerAsciiDocAttributes,
{ logger: assemblyModel.logger, mdc: configMdc }
)
const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
attributes: mergedAsciiDocAttributes,
})
assemblyModel.outDirname = computeOut.call(contentCatalog, {
component: componentName,
version,
family: 'export',
relative: '.index.adoc',
}).dirname
assemblyModel.filetype = assemblerAsciiDocAttributes['assembler-filetype']
assemblyModel.siteRoot =
siteRoot === undefined
? (siteRoot ??= ((val) => {
if (!val) return null
if (val.charAt(val.length - 1) === '/') val = val.slice(0, val.length - 1)
if (!val || val.charAt() === '/') return { path: val }
return { url: val, path: extractUrlPath(val) }
})(mergedAsciiDocAttributes['site-url'] || mergedAsciiDocAttributes['primary-site-url']))
: siteRoot
if (assemblyModel.filetype === 'html') {
let linkRefStyle = assemblyModel.linkReferenceStyle
if (linkRefStyle === 'absolute' && siteRoot?.url == null) linkRefStyle = 'root-relative'
if (linkRefStyle === 'root-relative' && siteRoot?.path == null) linkRefStyle = 'relative'
assemblyModel.linkReferenceStyle = linkRefStyle
} else if (!(assemblyModel.filetype === 'pdf' && assemblyModel.linkReferenceStyle === 'relative')) {
assemblyModel.linkReferenceStyle = 'absolute'
}
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
}
function collateAsciiDocAttributes (collated, additional, { logger, mdc }) {
Object.entries(additional).forEach(([name, val]) => {
if (val && val.constructor === String) {
let alias
val = val.replace(ATTR_REF_RX, (ref, refname) => {
if (ref.charAt() === '\\') return ref.substr(1)
const refval = collated[refname]
if (refval == null || refval === false) {
if (refname in collated && ref === val) {
alias = refval
} else if (collated['attribute-missing'] === 'warn') {
if (logger) {
logger.warn(mdc, "Skipping reference to missing attribute '%s' in value of '%s' attribute", refname, name)
}
}
return ref
}
if (refval.constructor === String) return refval
if (ref !== val) return refval.toString()
alias = refval
return ref
})
if (alias !== undefined) val = alias
}
collated[name] = val
})
return collated
}
function extractUrlPath (url) {
if (!url) return ''
const urlPath = new URL(url).pathname
return urlPath === '/' ? '' : urlPath
}
module.exports = produceAssemblyFiles