@antora/assembler
Version:
An extension library for Antora that assembles content from multiple pages into a single AsciiDoc file to converted and publish.
144 lines (134 loc) • 6.4 kB
JavaScript
const filterComponentVersions = require('./filter-component-versions')
const produceAggregateDocument = require('./produce-aggregate-document')
const selectMutableAttributes = require('./select-mutable-attributes')
const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfig) {
const { insertStartPage, rootLevel, sectionMergeStrategy, asciidoc: assemblerAsciiDocConfig } = assemblerConfig
const assemblerAsciiDocAttributes = Object.assign({}, assemblerAsciiDocConfig.attributes)
const { doctype, revdate, 'source-highlighter': sourceHighlighter } = assemblerAsciiDocAttributes
delete assemblerAsciiDocAttributes.doctype
delete assemblerAsciiDocAttributes.revdate
delete assemblerAsciiDocAttributes['source-highlighter']
return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersions).reduce(
(accum, componentVersion) => {
const { name: componentName, version, title, navigation } = componentVersion
if (!navigation) return accum
const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
attributes: Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
})
const mergedAsciiDocAttributes = mergedAsciiDocConfig.attributes
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]}]`
image.out.assembled = true
})
const rootEntry = { content: title }
let startPage = contentCatalog.getComponentVersionStartPage(componentName, version)
if (startPage && startPage.src.component === componentName && startPage.src.version === version) {
if (insertStartPage && !includedInNav(navigation, startPage.pub.url)) {
Object.assign(rootEntry, { url: startPage.pub.url, urlType: 'internal' })
}
} else {
// Q: should we always use a reference page as startPage for computing mutableAttributes?
startPage = createFile({
component: componentVersion.name,
version: componentVersion.version,
relative: '.reference-page.adoc',
origin: (componentVersion.origins || [])[0],
})
}
const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
delete mutableAttributes.doctype
accum = accum.concat(
prepareOutlines(navigation, rootEntry, rootLevel).map((outline) =>
produceAggregateDocument(
loadAsciiDoc,
contentCatalog,
componentVersion,
outline,
doctype,
contentCatalog.getPages((page) => page.out),
mergedAsciiDocConfig,
mutableAttributes,
sectionMergeStrategy
)
)
)
mergedAsciiDocAttributes.doctype = doctype
sourceHighlighter
? (mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter)
: delete mergedAsciiDocAttributes['source-highlighter']
return accum
},
[]
)
}
function createFile (src) {
const familySegment = (src.family ??= 'page') + 's'
const relativeSegments = src.relative.split('/')
const segments = ['modules', (src.module ??= 'ROOT'), familySegment, ...relativeSegments]
const path = segments.join('/')
const moduleRootPath = Array(relativeSegments.length - 1)
.fill('..')
.join('/')
const outPath = [
src.component === 'ROOT' ? '' : src.component,
src.version,
src.module === 'ROOT' ? '' : src.module,
src.family === 'page' ? '' : '_' + familySegment,
src.family === 'page' ? src.relative.replace(/\.adoc$/, '.html') : src.relative,
]
.filter((it) => it)
.join('/')
return {
path,
dirname: path.slice(0, path.lastIndexOf('/')),
contents: src.contents ?? Buffer.alloc(0),
src,
out: { path: outPath },
pub: { url: '/' + outPath, moduleRootPath },
}
}
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(',')))
}
// when root level is 0, merge the navigation into the rootEntry
// when root level is 1, create navigation per navigation menu
// in this case, if there's only a single navigation menu with no title, promote each top-level item to a menu
function prepareOutlines (navigation, rootEntry, rootLevel) {
if (rootLevel === 0 || navigation.length === 1) {
let navBranch
if (navigation.length === 1) {
navBranch = navigation[0]
} else {
const items = navigation.reduce((accum, it) => accum.concat(it.content ? it : it.items), [])
navBranch = items.length ? { items } : {}
}
return rootLevel === 0 || navBranch.content ? [Object.assign(rootEntry, navBranch)] : navBranch.items
}
return navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), [rootEntry])
}
module.exports = produceAggregateDocuments