UNPKG

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