@antora/assembler 1.0.0-alpha.9 → 1.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,13 @@
1
1
  # Antora Assembler
2
2
 
3
- A library for writing Antora extensions that combine multiple pages into one.
3
+ Antora Assembler provides a JavaScript library and a set of Antora extensions for merging content in an Antora site into assembly files and converting those files to another format, such as PDF.
4
4
 
5
- Assembler works by constructing aggregate AsciiDoc documents from the pages based on the navigation tree per component version.
6
- It then invokes the specified callback to convert those documents to an output format.
7
- If a catalog is specified, Assembler pushes the converted files back into the catalog to be published as attachments alongside the other files in the site.
5
+ The core of Assembler (this package) handles merging AsciiDoc content from multiple pages into an assembly.
6
+ It repeats this process for each selected component version.
7
+ It then delegates to a function or command (e.g., asciidoctor-pdf) designated by an exporter extension (a separate package) to convert the assembly files to an export format, such as PDF, resulting in an export.
8
+ Assembler then publishes the export files along with the other files in the site.
9
+
10
+ The exports can then be viewed offline, either by the browser or a reader application.
8
11
 
9
12
  [Antora](https://antora.org) is a modular static site generator designed for creating documentation sites from AsciiDoc documents.
10
13
  The Assembler extends the feature set of Antora by providing the foundation for page aggregation.
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ autoload :JSON, 'json'
4
+
5
+ class JsonlFormatter < Asciidoctor::Logger::BasicFormatter
6
+ def call severity, time, progname, msg
7
+ entry = { 'time' => time.utc.to_i, 'level' => severity.downcase, 'name' => progname }
8
+ if ::String === msg
9
+ entry['msg'] = msg
10
+ else
11
+ loc = msg[:source_location]
12
+ entry['file'] = { 'path' => loc.file, 'line' => loc.lineno }
13
+ entry['msg'] = msg[:text]
14
+ end
15
+ (JSON.generate entry) + ?\n
16
+ end
17
+ end
18
+
19
+ proc do
20
+ logger = Asciidoctor::LoggerManager.logger
21
+ logger.formatter = JsonlFormatter.new
22
+ unless (progname = ::File.basename $PROGRAM_NAME).empty?
23
+ logger.progname = progname
24
+ end
25
+ end.call
@@ -1,46 +1,297 @@
1
1
  'use strict'
2
2
 
3
+ const computeOut = require('./util/compute-out')
4
+ const fs = require('node:fs')
5
+ const { promises: fsp } = fs
6
+ const LazyReadable = require('./util/lazy-readable')
3
7
  const loadConfig = require('./load-config')
4
- const produceAggregateDocuments = require('./produce-aggregate-documents')
8
+ const ospath = require('node:path')
9
+ const { posix: path } = ospath
10
+ const produceAssemblyFiles = require('./produce-assembly-files')
5
11
  const PromiseQueue = require('./util/promise-queue')
6
- const runCommand = require('./util/run-command')
12
+ const { stringify: toJSON } = JSON
7
13
 
8
- async function assembleContent (playbook, contentCatalog, convertDocument, { siteCatalog, configSource }) {
9
- // Q: could we get ContentCatalog#getComponentVersionStartPage() in Antora core?
10
- if (typeof contentCatalog.getComponentVersionStartPage !== 'function') {
11
- contentCatalog.getComponentVersionStartPage = function (component, version) {
12
- return this.resolvePage('index.adoc', { component, version })
13
- }
14
- }
14
+ const invariably = { new: () => ({}), void: () => undefined }
15
+ const PACKAGE_NAME = require('../package.json').name
16
+ const NEWLINE_RX = /(?:\r?\n)+/
17
+
18
+ async function assembleContent (playbook, contentCatalog, converter, { configSource, navigationCatalog }) {
15
19
  const assemblerConfig = await loadConfig(playbook, configSource)
16
20
  if (!assemblerConfig) return [] // TODO consider removing and doing this another way
17
- const generatorFunctions = this ? this.getFunctions() : {}
21
+ const context = isBound(this)
22
+ ? this
23
+ : {
24
+ getFunctions: invariably.new,
25
+ getLogger: invariably.void,
26
+ getVariables: invariably.new,
27
+ }
28
+ const generatorFunctions = context.getFunctions()
18
29
  const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
19
- const aggregateDocuments = produceAggregateDocuments(loadAsciiDoc, contentCatalog, assemblerConfig)
20
- if (!convertDocument) return aggregateDocuments
30
+ const {
31
+ convert = converter,
32
+ getDefaultCommand,
33
+ extname: targetExtname = '',
34
+ backend: targetBackend = targetExtname.slice(1),
35
+ embedReferenceStyle = 'relative',
36
+ mediaType: targetMediaType,
37
+ loggerName = PACKAGE_NAME,
38
+ } = converter ?? {}
39
+ const { assembly: assemblyConfig, build: buildConfig } = assemblerConfig
40
+ assemblyConfig.embedReferenceStyle = embedReferenceStyle
41
+ const { profile = targetBackend } = assemblyConfig
42
+ const intrinsicAttributes = { 'loader-assembler': '' }
43
+ buildConfig.cwd ??= process.cwd()
44
+ if (profile) {
45
+ buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler-${profile}`)
46
+ intrinsicAttributes[`assembler-profile-${profile}`] = ''
47
+ intrinsicAttributes['assembler-profile'] = profile
48
+ if (targetBackend) {
49
+ intrinsicAttributes[`assembler-backend-${targetBackend}`] = ''
50
+ intrinsicAttributes['assembler-backend'] = targetBackend
51
+ }
52
+ } else {
53
+ buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), 'build/assembler')
54
+ }
55
+ if (targetExtname) {
56
+ const targetFiletype = targetExtname.slice(1)
57
+ intrinsicAttributes[`assembler-filetype-${targetFiletype}`] = ''
58
+ intrinsicAttributes['assembler-filetype'] = targetFiletype
59
+ }
60
+ Object.assign(assemblerConfig.asciidoc.attributes, intrinsicAttributes)
61
+ const assemblyFiles = produceAssemblyFiles(
62
+ loadAsciiDoc,
63
+ contentCatalog,
64
+ assemblerConfig,
65
+ createResolveAssemblyModel(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog)
66
+ )
67
+ if (!(assemblyFiles.length && typeof convert === 'function')) return assemblyFiles
68
+ if (buildConfig.command == null && typeof getDefaultCommand === 'function') {
69
+ buildConfig.command = await getDefaultCommand(buildConfig.cwd)
70
+ }
21
71
  const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
22
- const buildConfig = assemblerConfig.build
23
- await prepareWorkspace(publishFiles, aggregateDocuments, contentCatalog, buildConfig)
24
- // TODO: pass more information to convertDocument so it doesn't have to compute internal stuff
25
- // Q: don't we need to pass in the combined/resolved AsciiDoc attributes per file or component version?
72
+ await prepareWorkspace(publishFiles, assemblyFiles, buildConfig)
73
+ const boundConvert = convert.bind(context)
26
74
  return new PromiseQueue({ concurrency: buildConfig.processLimit })
27
- .add(aggregateDocuments.map((doc) => () => convertDocument.call(this, doc, buildConfig, runCommand)))
75
+ .add(
76
+ assemblyFiles.map((doc) => async () => {
77
+ const relativeToOutput = embedReferenceStyle === 'output-relative'
78
+ const convertAttributes = prepareConvertAttributes(doc, targetExtname, relativeToOutput, buildConfig)
79
+ if (buildConfig.mkdirs) await fsp.mkdir(convertAttributes.outdir, { recursive: true, force: true })
80
+ return boundConvert(doc, convertAttributes, buildConfig).then((result) => {
81
+ const fileOrContents = resolveFileOrContents.call(context, result, convertAttributes, buildConfig, loggerName)
82
+ return coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents)
83
+ })
84
+ })
85
+ )
28
86
  .toPromise()
29
87
  .then((files) => {
30
- if (buildConfig.publish && siteCatalog) siteCatalog.addFiles(files)
31
- return files
88
+ if (!buildConfig.publish) {
89
+ for (const file of files) delete file.assembler.assembled
90
+ return files
91
+ }
92
+ const qualifyExports = buildConfig.qualifyExports
93
+ return files.map((file) => {
94
+ const pages = file.assembler.assembled.pages
95
+ delete file.assembler.assembled
96
+ file = contentCatalog.addFile(Object.assign(file, { out: computeOut.call(contentCatalog, file.src) }))
97
+ const extname = file.extname
98
+ const download = file.assembler.downloadStem + extname
99
+ if (qualifyExports) {
100
+ file.pub.url = '/' + (file.out.path = path.join(file.out.dirname, (file.out.basename = download)))
101
+ } else {
102
+ file.pub.download = download
103
+ }
104
+ pages.forEach((fragment, page) => {
105
+ const assemblerMeta = (page.assembler ??= {})
106
+ const exports = (assemblerMeta.exports ??= [])
107
+ const exportEntry = { fragment, file }
108
+ const insertIdx = exports.findIndex(
109
+ ({ file: candidate }) =>
110
+ !(candidate.src.component === page.src.component && candidate.src.version === page.src.version)
111
+ )
112
+ ~insertIdx ? exports.splice(insertIdx, 0, exportEntry) : exports.push(exportEntry)
113
+ const extnameProp = extname.slice(1)
114
+ if (extnameProp in assemblerMeta) return
115
+ Object.defineProperty(assemblerMeta, extnameProp, {
116
+ configurable: true,
117
+ enumerable: true,
118
+ get: findFirstExportWithExtname.bind(assemblerMeta, extname),
119
+ })
120
+ })
121
+ return file
122
+ })
32
123
  })
33
124
  }
34
125
 
126
+ function createResolveAssemblyModel (context, contentCatalog, common, intrinsicAttributes, navigationCatalog) {
127
+ const logger = context.getLogger?.(PACKAGE_NAME)
128
+ const { assemblerProfiles } = context.getVariables()
129
+ if (!assemblerProfiles) {
130
+ return (componentVersion) => {
131
+ const navigation =
132
+ navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version) ?? componentVersion.navigation
133
+ return Object.assign({ logger }, common, { navigation })
134
+ }
135
+ }
136
+ const boundSendToLog = sendToLog.bind(logger)
137
+ const { buildNavigation = require('@antora/navigation-builder') } = context.getFunctions()
138
+ return (componentVersion) => {
139
+ const componentVersionProfiles = assemblerProfiles.get(componentVersion.version + '@' + componentVersion.name)
140
+ const overrides =
141
+ componentVersionProfiles?.get(intrinsicAttributes['assembler-profile']) ?? componentVersionProfiles?.get() ?? {}
142
+ const { navFiles, messages } = overrides
143
+ const model = Object.assign({ logger }, common, overrides)
144
+ delete model.navFiles
145
+ delete model.messages
146
+ const navigationOverride = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version)
147
+ if (navigationOverride) return Object.assign(model, { navigation: navigationOverride })
148
+ messages?.forEach(boundSendToLog)
149
+ model.navigation = componentVersion.navigation
150
+ if (!navFiles) return model
151
+ if (!navFiles.length) return Object.assign(model, { navigation: [] })
152
+ const navigation_ = model.navigation
153
+ const asciidoc_ = componentVersion.asciidoc
154
+ componentVersion.asciidoc = Object.assign({}, asciidoc_, {
155
+ attributes: Object.assign({}, asciidoc_.attributes, intrinsicAttributes),
156
+ })
157
+ buildNavigation(
158
+ new Proxy(contentCatalog, {
159
+ get (target, property) {
160
+ const method = target[property]
161
+ if (property !== 'findBy') return method
162
+ return (criteria) => (toJSON(criteria) === '{"family":"nav"}' ? navFiles : method.call(target, criteria))
163
+ },
164
+ })
165
+ )
166
+ model.navigation = componentVersion.navigation
167
+ Object.assign(componentVersion, { asciidoc: asciidoc_, navigation: navigation_ })
168
+ return model
169
+ }
170
+ }
171
+
172
+ function prepareConvertAttributes (doc, targetExtname, relativeToOutput, buildConfig) {
173
+ const {
174
+ asciidoc: { attributes: docAttributes } = { attributes: {} },
175
+ extname: docfilesuffix,
176
+ path: reldocfile,
177
+ src: { family, relative },
178
+ } = doc
179
+ const { cwd = process.cwd(), dir = cwd } = buildConfig
180
+ const docname = family + '$' + relative.slice(0, relative.length - docfilesuffix.length)
181
+ const docfile = ospath.join(dir, reldocfile)
182
+ const outdir = ospath.dirname(docfile)
183
+ const docdir = relativeToOutput ? dir : outdir
184
+ const imagesdir = ''
185
+ const outfile = docfile.slice(0, docfile.length - docfilesuffix.length) + targetExtname
186
+ const attributes = Object.assign({}, docAttributes, {
187
+ docdir,
188
+ docfile,
189
+ docfilesuffix,
190
+ 'docname@': docname,
191
+ imagesdir,
192
+ outdir,
193
+ outfile,
194
+ outfilesuffix: targetExtname,
195
+ toArgs (optionFlag, command) {
196
+ const padCharRef = process.platform === 'win32' && command.startsWith('bundle exec ')
197
+ const args = []
198
+ for (let [name, val] of Object.entries(this)) {
199
+ if (val) {
200
+ val = `${name}=${padCharRef && typeof val.charAt === 'function' && val.charAt() === '&' ? ' ' : ''}${val}`
201
+ } else if (val === '') {
202
+ if (name === 'asciidoctor-log-integration') {
203
+ args.push('-r', require.resolve('#asciidoctor-log-adapter'))
204
+ continue
205
+ }
206
+ val = name
207
+ } else {
208
+ val = `!${name}${val === false ? '@' : ''}`
209
+ }
210
+ args.push(optionFlag, val)
211
+ }
212
+ return args
213
+ },
214
+ })
215
+ return Object.defineProperty(attributes, 'toArgs', { enumerable: false })
216
+ }
217
+
218
+ function coerceToExportFormat (originalFile, targetBackend, targetExtname, targetMediaType, fileOrContents) {
219
+ const file =
220
+ fileOrContents == null || Buffer.isBuffer(fileOrContents) || typeof fileOrContents.pipe === 'function'
221
+ ? Object.assign(originalFile, { contents: fileOrContents })
222
+ : fileOrContents
223
+ ;(file.assembler ??= {}).backend = targetBackend
224
+ if (file.extname === targetExtname) return file
225
+ const sourcePath = file.path
226
+ const sourceExtname = file.extname
227
+ const relativeWithoutExtname = file.src.relative.slice(0, file.src.relative.length - sourceExtname.length)
228
+ const newPath = sourcePath.slice(0, sourcePath.length - sourceExtname.length) + targetExtname
229
+ Object.assign(file, { mediaType: (file.src.mediaType = targetMediaType), path: newPath })
230
+ file.src.basename = path.basename((file.src.relative = relativeWithoutExtname + (file.src.extname = targetExtname)))
231
+ return file
232
+ }
233
+
234
+ function findFirstExportWithExtname (extname) {
235
+ return this.exports.find((it) => it.file.extname === extname)
236
+ }
237
+
35
238
  // TODO: if no workspace dir is defined, we shouldn't continue
36
- function prepareWorkspace (publishFiles, aggregateDocuments, contentCatalog, buildConfig) {
37
- const { dir, clean, keepAggregateSource } = buildConfig
38
- const files = contentCatalog.findBy({ family: 'image' }).filter(({ out }) => out?.assembled)
39
- if (keepAggregateSource) {
40
- files.push(...aggregateDocuments.map((file) => Object.assign(file, { out: { path: file.path } })))
239
+ function prepareWorkspace (publishFiles, assemblyFiles, buildConfig) {
240
+ const { dir, clean, keepSource } = buildConfig
241
+ const files = []
242
+ const outPaths = new Set()
243
+ for (const file of assemblyFiles) {
244
+ for (const asset of file.assembler.assembled.assets) {
245
+ if (outPaths.has(asset.out.path)) continue
246
+ files.push(asset)
247
+ outPaths.add(asset.out.path)
248
+ }
249
+ }
250
+ if (keepSource) files.push(...assemblyFiles.map((file) => Object.assign(file, { out: { path: file.path } })))
251
+ return publishFiles({ output: { clean, dir } }, { getFiles: () => files })
252
+ }
253
+
254
+ function resolveFileOrContents (convertResult, convertAttributes, buildConfig, loggerName) {
255
+ let fileOrContents
256
+ if (convertResult?.status != null) {
257
+ fileOrContents = 'file' in convertResult ? convertResult.file : convertResult.contents
258
+ let logger, match
259
+ if (buildConfig.stderrSink === 'log' && convertResult.stderr.length && (logger = this.getLogger(loggerName))) {
260
+ const docfile = convertAttributes.docfile
261
+ const command = buildConfig.command
262
+ const stderr = convertResult.stderr.toString().trimEnd()
263
+ stderr.split(NEWLINE_RX).forEach((line) => {
264
+ const ctx = { command, file: { path: docfile } }
265
+ if (line.charAt() === '{' && line.charAt(line.length - 1) === '}') {
266
+ const entry = JSON.parse(line)
267
+ if (entry.name) ctx.program = entry.name
268
+ if (entry.file?.line) ctx.line = entry.file.line
269
+ logger[entry.level](ctx, entry.msg)
270
+ } else if ((match = /^(.+):(\d+): warning: (.+)/.exec(line))) {
271
+ const [, scriptPath, lineno, msg] = match
272
+ ctx.stack = [{ file: { path: scriptPath }, line: parseInt(lineno, 10) }]
273
+ logger.warn(ctx, msg)
274
+ } else {
275
+ logger.info(ctx, line)
276
+ }
277
+ })
278
+ }
279
+ } else if (convertResult !== undefined) {
280
+ return convertResult
41
281
  }
42
- // TODO: site publisher should accept a single catalog
43
- return publishFiles({ output: { clean, dir } }, [{ getFiles: () => files }])
282
+ return fileOrContents === undefined
283
+ ? new LazyReadable(() => fs.createReadStream(convertAttributes.outfile))
284
+ : fileOrContents
285
+ }
286
+
287
+ function isBound (obj) {
288
+ if (obj == null) return false
289
+ for (const _ in obj) return true
290
+ return false
291
+ }
292
+
293
+ function sendToLog (levelAndArgs) {
294
+ if (this) this[levelAndArgs[0]].apply(this, levelAndArgs.slice(1))
44
295
  }
45
296
 
46
297
  module.exports = assembleContent
@@ -0,0 +1,93 @@
1
+ 'use strict'
2
+
3
+ function configure (context, ...args) {
4
+ internalConfigure.apply(context, args)
5
+ }
6
+
7
+ function internalConfigure (converter, config = {}, providers = {}) {
8
+ this.once('componentsRegistered', ({ contentCatalog, assemblerProfiles }) => {
9
+ if (assemblerProfiles) return
10
+ this.updateVariables({ assemblerProfiles: getAssemblerProfiles(contentCatalog) })
11
+ })
12
+
13
+ this.once('beforeProcess', ({ siteAsciiDocConfig }) => {
14
+ siteAsciiDocConfig.keepSource = true
15
+ })
16
+
17
+ this.once('navigationBuilt', async ({ playbook, contentCatalog }) => {
18
+ const { assembleContent = require('./assemble-content'), ...assembleContentConfig } = providers
19
+ assembleContentConfig.configSource ??= config.configFile
20
+ await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfig)
21
+ })
22
+ }
23
+
24
+ function getAssemblerProfiles (contentCatalog, assemblerProfiles = new Map()) {
25
+ contentCatalog.getComponents().forEach((component) => {
26
+ component.versions.forEach((componentVersion) => {
27
+ const source = componentVersion.nav?.origin
28
+ const assemblerConfig = getAssemblerConfigFromDescriptor(source?.descriptor)
29
+ if (!assemblerConfig) return
30
+ const componentVersionRef = `${componentVersion.version}@${componentVersion.name}`
31
+ const filesByPath = componentVersion.files.reduce((accum, it) => accum.set(it.path, it), new Map())
32
+ const componentVersionProfiles = assemblerConfig.reduce((accum, entry) => {
33
+ const data = {}
34
+ const profile = entry.profile ?? undefined
35
+ if (accum.has(profile)) return accum
36
+ const nav = entry.nav
37
+ Object.entries(entry).forEach(([key, val]) => {
38
+ if (key === 'profile' || key === 'nav') return
39
+ const camelKey = key.toLowerCase().replace(/[_-]([a-z0-9])/g, (_, l, idx) => (idx ? l.toUpperCase() : l))
40
+ data[camelKey] = val
41
+ })
42
+ if (nav) {
43
+ data.navFiles = (nav.length ? [...new Set(nav)] : nav).reduce((navFiles, path_) => {
44
+ const navFile = filesByPath.get(path_)
45
+ if (navFile) {
46
+ navFiles.push(initNavFile(navFile, component.name, componentVersion.version, navFiles.length))
47
+ } else {
48
+ ;(data.messages ??= []).push([
49
+ 'warn',
50
+ { source },
51
+ `Could not resolve nav file for ${profile || '<default>'} profile in ${componentVersionRef}: ${path_}`,
52
+ ])
53
+ }
54
+ return navFiles
55
+ }, [])
56
+ } else if (!Object.keys(data).length) {
57
+ return accum
58
+ }
59
+ return accum.set(profile, data)
60
+ }, new Map())
61
+ assemblerProfiles.set(componentVersionRef, componentVersionProfiles)
62
+ })
63
+ })
64
+ return assemblerProfiles
65
+ }
66
+
67
+ function getAssemblerConfigFromDescriptor (descriptor = {}) {
68
+ const assemblerConfig = descriptor.ext?.assembler
69
+ if (!assemblerConfig) return
70
+ if (!Array.isArray(assemblerConfig)) return [assemblerConfig]
71
+ if (assemblerConfig.length) return assemblerConfig
72
+ }
73
+
74
+ function initNavFile (file, component, version, index) {
75
+ const src = Object.assign({}, file.src, { component, version, module: 'ROOT', family: 'nav' })
76
+ const filePathSegments = file.path.split('/')
77
+ let relativeStartIdx = 0
78
+ if (filePathSegments[0] === 'modules') {
79
+ relativeStartIdx += 1
80
+ if (filePathSegments.length > 2) {
81
+ relativeStartIdx += 1
82
+ src.module = filePathSegments[1]
83
+ if (filePathSegments.length > 3 && filePathSegments[2] === 'partials') {
84
+ relativeStartIdx += 1
85
+ src.family = 'partial'
86
+ }
87
+ }
88
+ }
89
+ src.relative = filePathSegments.slice(relativeStartIdx).join('/')
90
+ return Object.assign(new file.constructor(file), { nav: { index }, src })
91
+ }
92
+
93
+ module.exports = configure
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const assembleContent = require('./assemble-content')
4
- const runCommand = require('./util/run-command')
4
+ const configure = require('./configure')
5
5
 
6
- module.exports = { assembleContent, runCommand }
6
+ module.exports = { assembleContent, configure, configureAssembler: configure }
@@ -5,19 +5,31 @@ const fsp = require('node:fs/promises')
5
5
  const os = require('node:os')
6
6
  const yaml = require('js-yaml')
7
7
 
8
+ const ASSEMBLY_KEYS = [
9
+ 'rootLevel',
10
+ 'insertStartPage',
11
+ 'sectionMergeStrategy',
12
+ 'linkReferenceStyle',
13
+ 'dropExplicitXrefText',
14
+ ]
15
+
8
16
  function loadConfig (playbook, configSource = './antora-assembler.yml') {
9
17
  return (
10
18
  configSource.constructor === Object
11
19
  ? Promise.resolve(configSource)
12
20
  : fsp
13
- .access((configSource = expandPath(configSource, { dot: playbook.dir })))
14
- .then(
15
- () => true,
16
- () => false
17
- )
18
- .then((exists) =>
19
- exists ? fsp.readFile(configSource).then((data) => camelCaseKeys(yaml.load(data), ['asciidoc'])) : {}
20
- )
21
+ .access((configSource = expandPath(configSource, { dot: playbook.dir })))
22
+ .then(
23
+ () => true,
24
+ () => false
25
+ )
26
+ .then((exists) =>
27
+ exists
28
+ ? fsp
29
+ .readFile(configSource)
30
+ .then((data) => Object.assign(camelCaseKeys(yaml.load(data), ['asciidoc']), { file: configSource }))
31
+ : {}
32
+ )
21
33
  ).then((config) => {
22
34
  if (config.enabled === false) return undefined
23
35
  let asciidocAttrs
@@ -26,38 +38,69 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
26
38
  } else if (!(asciidocAttrs = config.asciidoc.attributes)) {
27
39
  config.asciidoc.attributes = asciidocAttrs = {}
28
40
  }
29
- if (!('doctype' in asciidocAttrs)) asciidocAttrs.doctype = 'book'
30
41
  if (!('revdate' in asciidocAttrs)) asciidocAttrs.revdate = getLocalDate()
31
42
  asciidocAttrs['page-partial'] = null
32
- asciidocAttrs['loader-assembler'] = ''
33
- if (config.componentVersions == null) {
34
- config.componentVersions = ['*']
35
- } else if (typeof config.componentVersions === 'string') {
36
- config.componentVersions = config.componentVersions.split(', ')
43
+ const remapComponentVersionsKey = !('componentVersionFilter' in config)
44
+ const componentVersionFilter = (config.componentVersionFilter ??= {})
45
+ if (remapComponentVersionsKey && 'componentVersions' in config) {
46
+ componentVersionFilter.names = config.componentVersions
47
+ delete config.componentVersions
48
+ }
49
+ if (componentVersionFilter.names == null) {
50
+ componentVersionFilter.names = ['*']
51
+ } else if (typeof componentVersionFilter.names === 'string') {
52
+ componentVersionFilter.names = componentVersionFilter.names.split(', ')
53
+ }
54
+ const remapAssemblyKeys = !('assembly' in config)
55
+ const assembly = (config.assembly ??= {})
56
+ if (remapAssemblyKeys) {
57
+ for (const key of ASSEMBLY_KEYS) {
58
+ if (!(key in config)) continue
59
+ assembly[key] = config[key]
60
+ delete config[key]
61
+ }
62
+ }
63
+ if (!('doctype' in assembly)) assembly.doctype = 'doctype' in asciidocAttrs ? asciidocAttrs.doctype : 'book'
64
+ delete asciidocAttrs.doctype
65
+ if (!('rootLevel' in assembly)) assembly.rootLevel = 0
66
+ if (!('insertStartPage' in assembly)) assembly.insertStartPage = true
67
+ if (['discrete', 'fuse', 'enclose'].indexOf(assembly.sectionMergeStrategy) < 0) {
68
+ assembly.sectionMergeStrategy = 'discrete'
37
69
  }
38
- if (!('rootLevel' in config)) config.rootLevel = 0
39
- if (!('insertStartPage' in config)) config.insertStartPage = true
40
- if (['discrete', 'fuse', 'enclose'].indexOf(config.sectionMergeStrategy) < 0) {
41
- config.sectionMergeStrategy = 'discrete'
70
+ if (['relative', 'root-relative', 'absolute'].indexOf(assembly.linkReferenceStyle) < 0) {
71
+ assembly.linkReferenceStyle = 'absolute'
42
72
  }
43
- const build = config.build || (config.build = {})
73
+ if (['always', 'if-redundant', 'never'].indexOf(assembly.dropExplicitXrefText) < 0) {
74
+ assembly.dropExplicitXrefText = 'never'
75
+ }
76
+ const build = (config.build ??= {})
44
77
  if (build.dir === '$' + '{playbook.output.dir}') {
45
- //build.dir = playbook.output.dir
46
78
  throw new Error('Not implemented')
47
- } else {
48
- build.dir = expandPath(build.dir || './build/assembler', { dot: playbook.dir })
49
79
  }
50
- build.cwd = playbook.dir // use playbook.dir for the purpose of finding and loading require scripts
80
+ build.dir &&= expandPath(build.dir, { dot: playbook.dir })
81
+ build.cwd = playbook.dir // use playbook.dir for finding and loading require scripts
51
82
  if (!('clean' in build) && 'output' in playbook) build.clean = playbook.output.clean
52
83
  if (!('publish' in build)) build.publish = true
84
+ if ('keepAggregateSource' in build) {
85
+ build.keepSource = build.keepAggregateSource
86
+ delete build.keepAggregateSource
87
+ }
53
88
  if (!build.processLimit) {
54
89
  build.processLimit = 'processLimit' in build ? Infinity : Math.round(os.cpus().length * 0.5)
55
90
  }
91
+ if ('stderr' in build) {
92
+ if (build.stderr === 'log') {
93
+ build.stderr = 'buffer'
94
+ build.stderrSink = 'log'
95
+ } else if (!['ignore', 'print'].includes(build.stderr)) {
96
+ delete build.stderr
97
+ }
98
+ }
56
99
  return config
57
100
  })
58
101
  }
59
102
 
60
- function camelCaseKeys (o, stopPaths = [], p) {
103
+ function camelCaseKeys (o, stopPaths = [], p = undefined) {
61
104
  if (Array.isArray(o)) return o.map((it) => camelCaseKeys(it, stopPaths, p))
62
105
  if (o == null || o.constructor !== Object) return o
63
106
  const pathPrefix = p ? p + '.' : ''