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.

203 lines (193 loc) 8.75 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 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