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.

217 lines (206 loc) 9.53 kB
'use strict' 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