UNPKG

@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
'use strict' 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