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.

235 lines (223 loc) 10.1 kB
'use strict' const computeOut = require('./util/compute-out') const fs = require('node:fs') const { promises: fsp } = fs const LazyReadable = require('./util/lazy-readable') const loadConfig = require('./load-config') const ospath = require('node:path') const { posix: path } = ospath const produceAssemblyFiles = require('./produce-assembly-files') const PromiseQueue = require('./util/promise-queue') const { stringify: toJSON } = JSON const invariably = { new: () => ({}), void: () => undefined } const PACKAGE_NAME = require('../package.json').name async function assembleContent (playbook, contentCatalog, converter, { configSource, navigationCatalog }) { const assemblerConfig = await loadConfig(playbook, configSource) if (!assemblerConfig) return [] // TODO consider removing and doing this another way const context = isBound(this) ? this : { getFunctions: invariably.new, getLogger: invariably.void, getVariables: invariably.new, } const generatorFunctions = context.getFunctions() const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions const targetBackend = converter == null ? undefined : (converter.backend ?? converter.extname?.slice(1)) const { assembly: assemblyConfig, build: buildConfig } = assemblerConfig const { profile = targetBackend } = assemblyConfig const intrinsicAttributes = { 'loader-assembler': '' } if (profile) { buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler-${profile}`) intrinsicAttributes[`assembler-profile-${profile}`] = '' intrinsicAttributes['assembler-profile'] = profile if (targetBackend) { intrinsicAttributes[`assembler-backend-${targetBackend}`] = '' intrinsicAttributes['assembler-backend'] = targetBackend } } else { buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), 'build/assembler') } Object.assign(assemblerConfig.asciidoc.attributes, intrinsicAttributes) const assemblyFiles = produceAssemblyFiles( loadAsciiDoc, contentCatalog, assemblerConfig, createResolveAssemblyModel(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog) ) const { convert = converter, extname: targetExtname, mediaType: targetMediaType } = converter ?? {} if (!(convert && assemblyFiles.length)) return assemblyFiles const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions await prepareWorkspace(publishFiles, assemblyFiles, contentCatalog, buildConfig) const boundConvert = convert.bind(context) return new PromiseQueue({ concurrency: buildConfig.processLimit }) .add( assemblyFiles.map((doc) => async () => { const convertAttributes = prepareConvertAttributes(doc, targetExtname, buildConfig) if (buildConfig.mkdirs) await fsp.mkdir(convertAttributes.outdir, { recursive: true, force: true }) return boundConvert(doc, convertAttributes, buildConfig).then( (fileOrContents = new LazyReadable(() => fs.createReadStream(convertAttributes.outfile))) => { return coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents) } ) }) ) .toPromise() .then((files) => { if (!buildConfig.publish) { for (const file of files) delete file.assembler.assembled return files } const qualifyExports = buildConfig.qualifyExports return files.map((file) => { const pages = file.assembler.assembled.pages delete file.assembler.assembled file = contentCatalog.addFile(Object.assign(file, { out: computeOut.call(contentCatalog, file.src) })) const extname = file.extname const download = file.assembler.downloadStem + extname if (qualifyExports) { file.pub.url = '/' + (file.out.path = path.join(file.out.dirname, (file.out.basename = download))) } else { file.pub.download = download } pages.forEach((fragment, page) => { const assemblerMeta = (page.assembler ??= {}) ;(assemblerMeta.exports ??= []).push({ fragment, file }) const extnameProp = extname.slice(1) if (extnameProp in assemblerMeta) return Object.defineProperty(assemblerMeta, extnameProp, { configurable: true, enumerable: true, get: findFirstExportWithExtname.bind(assemblerMeta, extname), }) }) return file }) }) } function createResolveAssemblyModel (context, contentCatalog, shared, intrinsicAttributes, navigationCatalog) { const { assemblerProfiles } = context.getVariables() if (!assemblerProfiles) { return (componentVersion) => { const navigation = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version) ?? componentVersion.navigation return Object.assign({}, shared, { navigation }) } } const boundSendToLog = sendToLog.bind(context.getLogger ? context.getLogger(PACKAGE_NAME) : undefined) const { buildNavigation = require('@antora/navigation-builder') } = context.getFunctions() return (componentVersion) => { const componentVersionProfiles = assemblerProfiles.get(componentVersion.version + '@' + componentVersion.name) const overrides = componentVersionProfiles?.get(intrinsicAttributes['assembler-profile']) ?? componentVersionProfiles?.get() ?? {} const { navFiles, messages } = overrides const model = Object.assign({}, shared, overrides) delete model.navFiles delete model.messages const navigationOverride = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version) if (navigationOverride) return Object.assign(model, { navigation: navigationOverride }) messages?.forEach(boundSendToLog) model.navigation = componentVersion.navigation if (!navFiles) return model if (!navFiles.length) return Object.assign(model, { navigation: [] }) const navigation_ = model.navigation const asciidoc_ = componentVersion.asciidoc componentVersion.asciidoc = Object.assign({}, asciidoc_, { attributes: Object.assign({}, asciidoc_.attributes, intrinsicAttributes), }) buildNavigation( new Proxy(contentCatalog, { get (target, property) { const method = target[property] if (property !== 'findBy') return method return (criteria) => (toJSON(criteria) === '{"family":"nav"}' ? navFiles : method.call(target, criteria)) }, }) ) model.navigation = componentVersion.navigation Object.assign(componentVersion, { asciidoc: asciidoc_, navigation: navigation_ }) return model } } function prepareConvertAttributes (doc, targetExtname, buildConfig) { const { asciidoc: { attributes: docAttributes } = { attributes: {} }, extname: docfilesuffix, path: reldocfile, src: { family, relative }, } = doc const { cwd = process.cwd(), dir = cwd } = buildConfig const docname = family + '$' + relative.slice(0, relative.length - docfilesuffix.length) const docfile = ospath.join(dir, reldocfile) const docdir = dir const imagesdir = '' const outfile = docfile.slice(0, docfile.length - docfilesuffix.length) + targetExtname const attributes = Object.assign({}, docAttributes, { docdir, docfile, docfilesuffix, 'docname@': docname, imagesdir, outdir: ospath.dirname(docfile), outfile, outfilesuffix: targetExtname, toArgs (optionFlag, command) { const padCharRef = process.platform === 'win32' && command.startsWith('bundle exec ') const args = [] for (let [name, val] of Object.entries(this)) { if (val) { val = `${name}=${padCharRef && typeof val.charAt === 'function' && val.charAt() === '&' ? ' ' : ''}${val}` } else if (val === '') { val = name } else { val = `!${name}${val === false ? '@' : ''}` } args.push('-a', val) } return args }, }) return Object.defineProperty(attributes, 'toArgs', { enumerable: false }) } function coerceToExportFormat (originalFile, targetBackend, targetExtname, targetMediaType, fileOrContents) { const file = fileOrContents == null || Buffer.isBuffer(fileOrContents) || typeof fileOrContents.pipe === 'function' ? Object.assign(originalFile, { contents: fileOrContents }) : fileOrContents ;(file.assembler ??= {}).backend = targetBackend if (file.extname === targetExtname) return file const sourcePath = file.path const sourceExtname = file.extname const relativeWithoutExtname = file.src.relative.slice(0, file.src.relative.length - sourceExtname.length) const newPath = sourcePath.slice(0, sourcePath.length - sourceExtname.length) + targetExtname Object.assign(file, { mediaType: (file.src.mediaType = targetMediaType), path: newPath }) file.src.basename = path.basename((file.src.relative = relativeWithoutExtname + (file.src.extname = targetExtname))) return file } function findFirstExportWithExtname (extname) { return this.exports.find((it) => it.file.extname === extname) } // TODO: if no workspace dir is defined, we shouldn't continue function prepareWorkspace (publishFiles, assemblyFiles, contentCatalog, buildConfig) { const { dir, clean, keepSource } = buildConfig const files = [] const outPaths = new Set() for (const file of assemblyFiles) { for (const asset of file.assembler.assembled.assets) { if (outPaths.has(asset.out.path)) continue files.push(asset) outPaths.add(asset.out.path) } } if (keepSource) files.push(...assemblyFiles.map((file) => Object.assign(file, { out: { path: file.path } }))) return publishFiles({ output: { clean, dir } }, { getFiles: () => files }) } function isBound (obj) { if (obj == null) return false for (const p in obj) return true return false } function sendToLog (levelAndArgs) { if (this) this[levelAndArgs[0]].apply(this, levelAndArgs.slice(1)) } module.exports = assembleContent