@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.
203 lines (193 loc) • 8.75 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
function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, selectAssemblyProfile) {
const { assembly: assemblyConfig } = assemblerConfig
selectAssemblyProfile ??= (componentVersion) => ({
attributes: assemblyConfig.attributes,
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,
revdate: assemblyConfig.revdate,
logger: assemblyConfig.logger, // for tests
})
const baseAssemblyAttributes = assemblyConfig.attributes
const publishableFiles = contentCatalog.getFiles().filter((file) => file.out)
let siteRoot
const configMdc = assemblerConfig.file ? { file: { path: assemblerConfig.file } } : {}
return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersionFilter).reduce(
(accum, componentVersion) => {
const assemblyModel = selectAssemblyProfile(componentVersion)
const { attributes: assemblyAttributes, logger, navigation, rootLevel } = assemblyModel
if (!navigation) return accum
const contextualLogger = logger ? { warn: logger.warn.bind(logger, configMdc) } : undefined
const { name: componentName, version, title } = componentVersion
const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
let sourceHighlighter
if ('source-highlighter' in assemblyAttributes) {
sourceHighlighter = assemblyAttributes['source-highlighter']
delete assemblyAttributes['source-highlighter']
}
const mergedAsciiDocAttributes = collateAsciiDocAttributes(
Object.assign({}, componentVersionAsciiDocConfig.attributes),
assemblyAttributes,
contextualLogger
)
const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
attributes: mergedAsciiDocAttributes,
})
assemblyModel.filetype = baseAssemblyAttributes['assembler-filetype']
const outDirname = (assemblyModel.outDirname = computeOut.call(contentCatalog, {
component: componentName,
version,
family: 'export',
relative: '.index.adoc',
}).dirname)
assemblyModel.pubRoot = outDirname ? '/' + outDirname : ''
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 rootEntry = { content: title }
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)
prepareOutlines(navigation, rootEntry, rootLevel).reduce((any, outline) => {
const assemblyFile = produceAssemblyFile(
loadAsciiDoc,
contentCatalog,
componentVersion,
outline,
publishableFiles,
mergedAsciiDocConfig,
mutableAttributes,
assemblyModel
)
if (!assemblyFile) return any
// NOTE restore source highlighter for conversion if defined in Assembler config
if (sourceHighlighter !== undefined) {
assemblyFile.asciidoc.attributes['source-highlighter'] = sourceHighlighter
} else if (assemblyModel.filetype !== 'html') {
delete assemblyFile.asciidoc.attributes['source-highlighter']
}
return !!accum.push(assemblyFile)
}, false)
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 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) {
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') {
logger?.warn("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