@antora/assembler 1.0.0-beta.2 → 1.0.0-beta.20

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.
@@ -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
@@ -5,18 +5,30 @@ const fs = require('node:fs')
5
5
  const { promises: fsp } = fs
6
6
  const LazyReadable = require('./util/lazy-readable')
7
7
  const loadConfig = require('./load-config')
8
+ const logCommand = require('./log-command')
8
9
  const ospath = require('node:path')
9
10
  const { posix: path } = ospath
10
11
  const produceAssemblyFiles = require('./produce-assembly-files')
11
12
  const PromiseQueue = require('./util/promise-queue')
13
+ const runCommand = require('@antora/run-command-helper')
12
14
  const { stringify: toJSON } = JSON
13
15
 
14
16
  const invariably = { new: () => ({}), void: () => undefined }
15
17
  const PACKAGE_NAME = require('../package.json').name
18
+ const NEWLINE_RX = /(?:\r?\n)+/
16
19
 
17
20
  async function assembleContent (playbook, contentCatalog, converter, { configSource, navigationCatalog }) {
18
- const assemblerConfig = await loadConfig(playbook, configSource)
19
- if (!assemblerConfig) return [] // TODO consider removing and doing this another way
21
+ const {
22
+ convert = converter,
23
+ getDefaultCommand,
24
+ extname: targetExtname = '',
25
+ backend: targetBackend = targetExtname.slice(1),
26
+ embedReferenceStyle = 'relative',
27
+ mediaType: targetMediaType,
28
+ loggerName = PACKAGE_NAME,
29
+ } = converter ?? {}
30
+ const assemblerConfig = await loadConfig.call(this, playbook, configSource, '-' + targetBackend)
31
+ if (assemblerConfig.enabled === false) return []
20
32
  const context = isBound(this)
21
33
  ? this
22
34
  : {
@@ -26,12 +38,13 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
26
38
  }
27
39
  const generatorFunctions = context.getFunctions()
28
40
  const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
29
- const targetBackend = converter == null ? undefined : (converter.backend ?? converter.extname?.slice(1))
30
41
  const { assembly: assemblyConfig, build: buildConfig } = assemblerConfig
31
- const { profile = targetBackend } = assemblyConfig
42
+ assemblyConfig.embedReferenceStyle = embedReferenceStyle
43
+ const profile = (assemblyConfig.profile ??= targetBackend)
32
44
  const intrinsicAttributes = { 'loader-assembler': '' }
45
+ buildConfig.cwd ??= process.cwd()
33
46
  if (profile) {
34
- buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler-${profile}`)
47
+ buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler/${profile}`)
35
48
  intrinsicAttributes[`assembler-profile-${profile}`] = ''
36
49
  intrinsicAttributes['assembler-profile'] = profile
37
50
  if (targetBackend) {
@@ -39,30 +52,38 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
39
52
  intrinsicAttributes['assembler-backend'] = targetBackend
40
53
  }
41
54
  } else {
42
- buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), 'build/assembler')
55
+ buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), 'build/assembler/_')
56
+ }
57
+ if (targetExtname) {
58
+ const targetFiletype = targetExtname.slice(1)
59
+ intrinsicAttributes[`assembler-filetype-${targetFiletype}`] = ''
60
+ intrinsicAttributes['assembler-filetype'] = targetFiletype
43
61
  }
44
- Object.assign(assemblerConfig.asciidoc.attributes, intrinsicAttributes)
62
+ Object.assign(assemblyConfig.attributes, intrinsicAttributes)
45
63
  const assemblyFiles = produceAssemblyFiles(
46
64
  loadAsciiDoc,
47
65
  contentCatalog,
48
66
  assemblerConfig,
49
- createResolveAssemblyModel(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog)
67
+ generateSelectAssemblyProfile(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog)
50
68
  )
51
- const { convert = converter, extname: targetExtname, mediaType: targetMediaType } = converter ?? {}
52
- if (!(convert && assemblyFiles.length)) return assemblyFiles
69
+ if (!(assemblyFiles.length && typeof convert === 'function')) return assemblyFiles
70
+ if (buildConfig.command == null && typeof getDefaultCommand === 'function') {
71
+ buildConfig.command = await getDefaultCommand(buildConfig.cwd)
72
+ }
53
73
  const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
54
- await prepareWorkspace(publishFiles, assemblyFiles, contentCatalog, buildConfig)
74
+ await prepareWorkspace(publishFiles, assemblyFiles, buildConfig)
75
+ const helpers = { logCommand: logCommand.bind(context, loggerName), runCommand }
55
76
  const boundConvert = convert.bind(context)
56
77
  return new PromiseQueue({ concurrency: buildConfig.processLimit })
57
78
  .add(
58
79
  assemblyFiles.map((doc) => async () => {
59
- const convertAttributes = prepareConvertAttributes(doc, targetExtname, buildConfig)
80
+ const relativeToOutput = embedReferenceStyle === 'output-relative'
81
+ const convertAttributes = prepareConvertAttributes(doc, targetExtname, relativeToOutput, assemblerConfig)
60
82
  if (buildConfig.mkdirs) await fsp.mkdir(convertAttributes.outdir, { recursive: true, force: true })
61
- return boundConvert(doc, convertAttributes, buildConfig).then(
62
- (fileOrContents = new LazyReadable(() => fs.createReadStream(convertAttributes.outfile))) => {
63
- return coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents)
64
- }
65
- )
83
+ return boundConvert(doc, convertAttributes, buildConfig, helpers).then((result) => {
84
+ const fileOrContents = resolveFileOrContents.call(context, result, convertAttributes, buildConfig, loggerName)
85
+ return coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents)
86
+ })
66
87
  })
67
88
  )
68
89
  .toPromise()
@@ -85,7 +106,13 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
85
106
  }
86
107
  pages.forEach((fragment, page) => {
87
108
  const assemblerMeta = (page.assembler ??= {})
88
- ;(assemblerMeta.exports ??= []).push({ fragment, file })
109
+ const exports = (assemblerMeta.exports ??= [])
110
+ const exportEntry = { fragment, file }
111
+ const insertIdx = exports.findIndex(
112
+ ({ file: candidate }) =>
113
+ !(candidate.src.component === page.src.component && candidate.src.version === page.src.version)
114
+ )
115
+ ~insertIdx ? exports.splice(insertIdx, 0, exportEntry) : exports.push(exportEntry)
89
116
  const extnameProp = extname.slice(1)
90
117
  if (extnameProp in assemblerMeta) return
91
118
  Object.defineProperty(assemblerMeta, extnameProp, {
@@ -99,71 +126,97 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
99
126
  })
100
127
  }
101
128
 
102
- function createResolveAssemblyModel (context, contentCatalog, shared, intrinsicAttributes, navigationCatalog) {
129
+ /**
130
+ * Generates a function that selects the active assembly profile, initializes an assembly model using
131
+ * the keys from the profile as well as any inherited shared keys, builds the navigation for the assembly, and
132
+ * returns the initialized assembly model. The assembly model is further populated after the call to this function.
133
+ */
134
+ function generateSelectAssemblyProfile (context, contentCatalog, baseModel, intrinsicAttributes, navigationCatalog) {
135
+ const logger = context.getLogger?.(PACKAGE_NAME)
103
136
  const { assemblerProfiles } = context.getVariables()
104
137
  if (!assemblerProfiles) {
105
138
  return (componentVersion) => {
106
139
  const navigation =
107
140
  navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version) ?? componentVersion.navigation
108
- return Object.assign({}, shared, { navigation })
141
+ return Object.assign({}, baseModel, { attributes: Object.assign({}, baseModel.attributes), navigation, logger })
109
142
  }
110
143
  }
111
- const boundSendToLog = sendToLog.bind(context.getLogger ? context.getLogger(PACKAGE_NAME) : undefined)
112
- const { buildNavigation = require('@antora/navigation-builder') } = context.getFunctions()
144
+ const boundSendToLog = sendToLog.bind(logger)
145
+ const {
146
+ buildNavigation = require('@antora/navigation-builder'),
147
+ buildAlternateNavigation = buildNavigation.buildAlternateNavigation ??
148
+ buildAlternateNavigationShim.bind(null, buildNavigation),
149
+ } = context.getFunctions()
113
150
  return (componentVersion) => {
151
+ const attributes = Object.assign({}, baseModel.attributes)
114
152
  const componentVersionProfiles = assemblerProfiles.get(componentVersion.version + '@' + componentVersion.name)
115
- const overrides =
116
- componentVersionProfiles?.get(intrinsicAttributes['assembler-profile']) ?? componentVersionProfiles?.get()
153
+ const overrides = componentVersionProfiles?.get(baseModel.profile) ?? componentVersionProfiles?.get() ?? {}
117
154
  const { navFiles, messages } = overrides
118
- const model = Object.assign({}, shared, overrides)
155
+ Object.entries(overrides.attributes ?? {}).forEach(([name, val]) => (attributes[name] = val))
156
+ const model = Object.assign({}, baseModel, overrides, { attributes, logger })
119
157
  delete model.navFiles
120
158
  delete model.messages
121
159
  const navigationOverride = navigationCatalog?.getNavigation(componentVersion.name, componentVersion.version)
122
160
  if (navigationOverride) return Object.assign(model, { navigation: navigationOverride })
123
161
  messages?.forEach(boundSendToLog)
124
- model.navigation = componentVersion.navigation
125
- if (!navFiles) return model
162
+ if (!navFiles) return Object.assign(model, { navigation: componentVersion.navigation })
126
163
  if (!navFiles.length) return Object.assign(model, { navigation: [] })
127
- const navigation_ = model.navigation
128
- const asciidoc_ = componentVersion.asciidoc
129
- componentVersion.asciidoc = Object.assign({}, asciidoc_, {
130
- attributes: Object.assign({}, asciidoc_.attributes, intrinsicAttributes),
164
+ model.navigation = buildAlternateNavigation(contentCatalog, componentVersion, navFiles, {
165
+ attributes: intrinsicAttributes,
131
166
  })
132
- buildNavigation(
133
- new Proxy(contentCatalog, {
134
- get (target, property) {
135
- const method = target[property]
136
- if (property !== 'findBy') return method
137
- return (criteria) => (toJSON(criteria) === '{"family":"nav"}' ? navFiles : method.call(target, criteria))
138
- },
139
- })
140
- )
141
- model.navigation = componentVersion.navigation
142
- Object.assign(componentVersion, { asciidoc: asciidoc_, navigation: navigation_ })
143
167
  return model
144
168
  }
145
169
  }
146
170
 
147
- function prepareConvertAttributes (doc, targetExtname, buildConfig) {
171
+ function buildAlternateNavigationShim (
172
+ buildNavigation,
173
+ contentCatalog,
174
+ componentVersion,
175
+ navFiles,
176
+ asciidocConfigOverrides
177
+ ) {
178
+ const { asciidoc: asciidoc_, navigation: navigation_ } = componentVersion
179
+ const asciidocConfig = (componentVersion.asciidoc = Object.assign({}, asciidoc_))
180
+ if (asciidocConfigOverrides) {
181
+ const attributesOverrides =
182
+ 'attributes' in asciidocConfigOverrides
183
+ ? { attributes: Object.assign({}, asciidocConfig.attributes, asciidocConfigOverrides.attributes) }
184
+ : undefined
185
+ Object.assign(asciidocConfig, asciidocConfigOverrides, attributesOverrides)
186
+ }
187
+ const contentCatalogProxy = new Proxy(contentCatalog, {
188
+ get (target, property) {
189
+ const method = target[property]
190
+ if (property !== 'findBy') return method
191
+ return (criteria) => (toJSON(criteria) === '{"family":"nav"}' ? navFiles : method.call(target, criteria))
192
+ },
193
+ })
194
+ buildNavigation(contentCatalogProxy)
195
+ const navigation = componentVersion.navigation
196
+ Object.assign(componentVersion, { asciidoc: asciidoc_, navigation: navigation_ })
197
+ return navigation
198
+ }
199
+
200
+ function prepareConvertAttributes (doc, targetExtname, relativeToOutput, assemblerConfig) {
148
201
  const {
149
202
  asciidoc: { attributes: docAttributes } = { attributes: {} },
150
203
  extname: docfilesuffix,
151
204
  path: reldocfile,
152
205
  src: { family, relative },
153
206
  } = doc
154
- const { cwd = process.cwd(), dir = cwd } = buildConfig
207
+ const { cwd = process.cwd(), dir = cwd } = assemblerConfig.build
155
208
  const docname = family + '$' + relative.slice(0, relative.length - docfilesuffix.length)
156
209
  const docfile = ospath.join(dir, reldocfile)
157
- const docdir = dir
158
- const imagesdir = ''
210
+ const outdir = ospath.dirname(docfile)
211
+ const docdir = relativeToOutput ? dir : outdir
159
212
  const outfile = docfile.slice(0, docfile.length - docfilesuffix.length) + targetExtname
160
- const attributes = Object.assign({}, docAttributes, {
213
+ const attributes = Object.assign({ revdate: `${assemblerConfig.assembly.revdate}@` }, docAttributes, {
161
214
  docdir,
162
215
  docfile,
163
216
  docfilesuffix,
164
217
  'docname@': docname,
165
- imagesdir,
166
- outdir: ospath.dirname(docfile),
218
+ imagesdir: '',
219
+ outdir,
167
220
  outfile,
168
221
  outfilesuffix: targetExtname,
169
222
  toArgs (optionFlag, command) {
@@ -173,11 +226,15 @@ function prepareConvertAttributes (doc, targetExtname, buildConfig) {
173
226
  if (val) {
174
227
  val = `${name}=${padCharRef && typeof val.charAt === 'function' && val.charAt() === '&' ? ' ' : ''}${val}`
175
228
  } else if (val === '') {
229
+ if (name === 'asciidoctor-log-integration') {
230
+ args.push('-r', require.resolve('#asciidoctor-log-adapter'))
231
+ continue
232
+ }
176
233
  val = name
177
234
  } else {
178
235
  val = `!${name}${val === false ? '@' : ''}`
179
236
  }
180
- args.push('-a', val)
237
+ args.push(optionFlag, val)
181
238
  }
182
239
  return args
183
240
  },
@@ -206,7 +263,7 @@ function findFirstExportWithExtname (extname) {
206
263
  }
207
264
 
208
265
  // TODO: if no workspace dir is defined, we shouldn't continue
209
- function prepareWorkspace (publishFiles, assemblyFiles, contentCatalog, buildConfig) {
266
+ function prepareWorkspace (publishFiles, assemblyFiles, buildConfig) {
210
267
  const { dir, clean, keepSource } = buildConfig
211
268
  const files = []
212
269
  const outPaths = new Set()
@@ -217,13 +274,57 @@ function prepareWorkspace (publishFiles, assemblyFiles, contentCatalog, buildCon
217
274
  outPaths.add(asset.out.path)
218
275
  }
219
276
  }
220
- if (keepSource) files.push(...assemblyFiles.map((file) => Object.assign(file, { out: { path: file.path } })))
277
+ if (keepSource) {
278
+ for (const file of assemblyFiles) {
279
+ file.src.contents = file.contents
280
+ file.out = { path: file.path }
281
+ files.push(file)
282
+ }
283
+ }
221
284
  return publishFiles({ output: { clean, dir } }, { getFiles: () => files })
222
285
  }
223
286
 
287
+ function resolveFileOrContents (convertResult, convertAttributes, buildConfig, loggerName) {
288
+ let fileOrContents
289
+ if (convertResult?.status != null) {
290
+ fileOrContents = 'file' in convertResult ? convertResult.file : convertResult.contents
291
+ let logger, match
292
+ if (buildConfig.stderrSink === 'log' && convertResult.stderr.length && (logger = this.getLogger(loggerName))) {
293
+ const docfile = convertAttributes.docfile
294
+ const command = buildConfig.command
295
+ const stderr = convertResult.stderr.toString().trimEnd()
296
+ stderr.split(NEWLINE_RX).forEach((line) => {
297
+ const ctx = { command, file: { path: docfile } }
298
+ if (line.charAt() === '{' && line.charAt(line.length - 1) === '}') {
299
+ const entry = JSON.parse(line)
300
+ if (entry.name) ctx.program = entry.name
301
+ if (entry.file?.line) ctx.line = entry.file.line
302
+ logger[entry.level](ctx, entry.msg)
303
+ } else if ((match = /^([^:]+):(\d+): warning: (.+)/.exec(line))) {
304
+ const [, scriptPath, lineno, msg] = match
305
+ ctx.stack = [{ file: { path: scriptPath }, line: parseInt(lineno, 10) }]
306
+ logger.warn(ctx, msg)
307
+ } else if ((match = /^asciidoctor: ([A-Z]+): (?:[^:]+: line (\d+): )?(.+)/.exec(line))) {
308
+ // NOTE we don't care about the filename in the message since it can only be docfile
309
+ const [, level, lineno, msg] = match
310
+ if (lineno) ctx.line = parseInt(lineno, 10)
311
+ logger[level === 'WARNING' ? 'warn' : level.toLowerCase()](ctx, msg)
312
+ } else {
313
+ logger.info(ctx, line)
314
+ }
315
+ })
316
+ }
317
+ } else if (convertResult !== undefined) {
318
+ return convertResult
319
+ }
320
+ return fileOrContents === undefined
321
+ ? new LazyReadable(() => fs.createReadStream(convertAttributes.outfile))
322
+ : fileOrContents
323
+ }
324
+
224
325
  function isBound (obj) {
225
326
  if (obj == null) return false
226
- for (const p in obj) return true
327
+ for (const _ in obj) return true
227
328
  return false
228
329
  }
229
330
 
package/lib/configure.js CHANGED
@@ -6,24 +6,46 @@ function configure (context, ...args) {
6
6
 
7
7
  function internalConfigure (converter, config = {}, providers = {}) {
8
8
  this.once('componentsRegistered', ({ contentCatalog, assemblerProfiles }) => {
9
- this.updateVariables(assemblerProfiles ? undefined : { assemblerProfiles: getAssemblerProfiles(contentCatalog) })
9
+ if (assemblerProfiles) return
10
+ this.updateVariables({ assemblerProfiles: getAssemblerProfiles(contentCatalog) })
10
11
  })
11
12
 
12
- this.once('beforeProcess', ({ siteAsciiDocConfig }) => {
13
- siteAsciiDocConfig.keepSource = true
14
- })
13
+ this.once('beforeProcess', enableKeepSource)
15
14
 
16
15
  this.once('navigationBuilt', async ({ playbook, contentCatalog }) => {
17
16
  const { assembleContent = require('./assemble-content'), ...assembleContentConfig } = providers
18
- assembleContentConfig.configSource ??= config.configFile
19
- await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfig)
17
+ if (assembleContentConfig.configSource?.constructor === Object) {
18
+ await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfig)
19
+ } else {
20
+ const singleConfig = !('configFiles' in config)
21
+ const configFiles = singleConfig ? config.configFile : config.configFiles
22
+ for (const configSource of Array.isArray(configFiles) ? configFiles : [configFiles]) {
23
+ const assembleContentConfigWithConfigSource = Object.assign({}, assembleContentConfig, { configSource })
24
+ await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfigWithConfigSource)
25
+ if (singleConfig) break
26
+ }
27
+ }
20
28
  })
21
29
  }
22
30
 
31
+ function enableKeepSource ({ siteAsciiDocConfig }) {
32
+ if (siteAsciiDocConfig.keepSource instanceof Boolean) return
33
+ siteAsciiDocConfig.keepSource = Object.assign(new Boolean(true), { oldValue: siteAsciiDocConfig.keepSource })
34
+ this.once('navigationBuilt', restoreKeepSource)
35
+ }
36
+
37
+ function restoreKeepSource ({ siteAsciiDocConfig, contentCatalog }) {
38
+ if (!(siteAsciiDocConfig.keepSource instanceof Boolean)) return
39
+ if ((siteAsciiDocConfig.keepSource = siteAsciiDocConfig.keepSource.oldValue)) return
40
+ contentCatalog.getPages((page) => delete page.src.contents)
41
+ }
42
+
23
43
  function getAssemblerProfiles (contentCatalog, assemblerProfiles = new Map()) {
24
44
  contentCatalog.getComponents().forEach((component) => {
25
45
  component.versions.forEach((componentVersion) => {
26
- const source = componentVersion.nav?.origin
46
+ const source =
47
+ componentVersion.nav?.origin ??
48
+ [...(componentVersion.origins ?? [])].find((it) => it.descriptor?.ext?.assembler)
27
49
  const assemblerConfig = getAssemblerConfigFromDescriptor(source?.descriptor)
28
50
  if (!assemblerConfig) return
29
51
  const componentVersionRef = `${componentVersion.version}@${componentVersion.name}`
@@ -64,14 +86,10 @@ function getAssemblerProfiles (contentCatalog, assemblerProfiles = new Map()) {
64
86
  }
65
87
 
66
88
  function getAssemblerConfigFromDescriptor (descriptor = {}) {
67
- let assemblerConfig = descriptor.ext?.assembler
89
+ const assemblerConfig = descriptor.ext?.assembler
68
90
  if (!assemblerConfig) return
69
- if (Array.isArray(assemblerConfig)) {
70
- if (!assemblerConfig.length) return
71
- } else {
72
- assemblerConfig = [assemblerConfig]
73
- }
74
- return assemblerConfig
91
+ if (!Array.isArray(assemblerConfig)) return [assemblerConfig]
92
+ if (assemblerConfig.length) return assemblerConfig
75
93
  }
76
94
 
77
95
  function initNavFile (file, component, version, index) {
@@ -36,12 +36,13 @@ function compilePatterns (patterns) {
36
36
  })
37
37
  }
38
38
 
39
- function filterComponentVersions (components, patterns) {
39
+ function filterComponentVersions (components, { names: patterns, prereleases = true }) {
40
40
  if (!patterns.length) return []
41
41
  const rxs = compilePatterns(patterns)
42
42
  return components.reduce((accum, { latest, versions }) => {
43
43
  accum.push(
44
44
  ...versions.filter((version) => {
45
+ if (!prereleases && version.prerelease && version !== latest) return false
45
46
  let matched
46
47
  for (const rx of rxs) {
47
48
  let voteIfMatched
@@ -5,29 +5,49 @@ const fsp = require('node:fs/promises')
5
5
  const os = require('node:os')
6
6
  const yaml = require('js-yaml')
7
7
 
8
- function loadConfig (playbook, configSource = './antora-assembler.yml') {
8
+ const ASSEMBLY_KEYS = [
9
+ 'rootLevel',
10
+ 'insertStartPage',
11
+ 'sectionMergeStrategy',
12
+ 'linkReferenceStyle',
13
+ 'dropExplicitXrefText',
14
+ ]
15
+ const CAMEL_CASE_STOP_PATHS = ['asciidoc.attributes', 'assembly.attributes']
16
+ const PACKAGE_NAME = require('../package.json').name
17
+
18
+ function loadConfig (playbook, configSource, preferredQualifier = '') {
19
+ let resolvedConfigSource
9
20
  return (
10
- configSource.constructor === Object
21
+ configSource?.constructor === Object
11
22
  ? Promise.resolve(configSource)
12
- : 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
- )
23
+ : fileExists(
24
+ (resolvedConfigSource = expandPath(configSource ?? `./antora-assembler${preferredQualifier}.yml`, {
25
+ dot: playbook.dir,
26
+ }))
27
+ )
28
+ .then((exists) => {
29
+ if (exists || configSource || !preferredQualifier) return exists
30
+ return fileExists(
31
+ (resolvedConfigSource = resolvedConfigSource.slice(0, -(preferredQualifier.length + 4)) + '.yml')
32
+ )
33
+ })
34
+ .then((exists) => {
35
+ if (!exists) {
36
+ let logger
37
+ if (configSource && (logger = this?.getLogger?.(PACKAGE_NAME))) {
38
+ const ctx = { file: { path: configSource } }
39
+ logger.warn(ctx, 'Could not resolve config file; reverting to default settings')
40
+ }
41
+ return {}
42
+ }
43
+ return fsp
44
+ .readFile(resolvedConfigSource)
45
+ .then((data) =>
46
+ Object.assign(camelCaseKeys(yaml.load(data), CAMEL_CASE_STOP_PATHS), { file: resolvedConfigSource })
47
+ )
48
+ })
21
49
  ).then((config) => {
22
- if (config.enabled === false) return undefined
23
- let asciidocAttrs
24
- if (!config.asciidoc) {
25
- config.asciidoc = { attributes: (asciidocAttrs = {}) }
26
- } else if (!(asciidocAttrs = config.asciidoc.attributes)) {
27
- config.asciidoc.attributes = asciidocAttrs = {}
28
- }
29
- if (!('revdate' in asciidocAttrs)) asciidocAttrs.revdate = getLocalDate()
30
- asciidocAttrs['page-partial'] = null
50
+ if (config.enabled === false) return config
31
51
  const remapComponentVersionsKey = !('componentVersionFilter' in config)
32
52
  const componentVersionFilter = (config.componentVersionFilter ??= {})
33
53
  if (remapComponentVersionsKey && 'componentVersions' in config) {
@@ -42,25 +62,49 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
42
62
  const remapAssemblyKeys = !('assembly' in config)
43
63
  const assembly = (config.assembly ??= {})
44
64
  if (remapAssemblyKeys) {
45
- for (const key of ['rootLevel', 'insertStartPage', 'sectionMergeStrategy']) {
65
+ for (const key of ASSEMBLY_KEYS) {
46
66
  if (!(key in config)) continue
47
67
  assembly[key] = config[key]
48
68
  delete config[key]
49
69
  }
50
70
  }
51
- if (!('doctype' in assembly)) assembly.doctype = 'doctype' in asciidocAttrs ? asciidocAttrs.doctype : 'book'
52
- delete asciidocAttrs.doctype
71
+ let assemblyAttrs
72
+ if ('attributes' in assembly) {
73
+ assemblyAttrs = assembly.attributes ?? {}
74
+ if ('asciidoc' in config) delete config.asciidoc
75
+ } else if ('asciidoc' in config) {
76
+ assemblyAttrs = assembly.attributes = config.asciidoc?.attributes ?? {}
77
+ delete config.asciidoc
78
+ } else {
79
+ assemblyAttrs = assembly.attributes = {}
80
+ }
81
+ assemblyAttrs['page-partial'] = null
82
+ config.asciidoc = Object.defineProperty({}, 'attributes', {
83
+ get: function () {
84
+ return this.assembly.attributes
85
+ }.bind(config),
86
+ })
87
+ if (!('doctype' in assembly)) assembly.doctype = 'doctype' in assemblyAttrs ? assemblyAttrs.doctype : 'book'
88
+ delete assemblyAttrs.doctype
53
89
  if (!('rootLevel' in assembly)) assembly.rootLevel = 0
54
90
  if (!('insertStartPage' in assembly)) assembly.insertStartPage = true
55
91
  if (['discrete', 'fuse', 'enclose'].indexOf(assembly.sectionMergeStrategy) < 0) {
56
92
  assembly.sectionMergeStrategy = 'discrete'
57
93
  }
94
+ if (['relative', 'root-relative', 'absolute'].indexOf(assembly.linkReferenceStyle) < 0) {
95
+ assembly.linkReferenceStyle = 'absolute'
96
+ }
97
+ if (['always', 'if-redundant', 'never'].indexOf(assembly.dropExplicitXrefText) < 0) {
98
+ assembly.dropExplicitXrefText = 'never'
99
+ }
100
+ assembly.revdate = getLocalDate()
58
101
  const build = (config.build ??= {})
59
102
  if (build.dir === '$' + '{playbook.output.dir}') {
60
103
  throw new Error('Not implemented')
61
104
  }
62
105
  build.dir &&= expandPath(build.dir, { dot: playbook.dir })
63
- build.cwd = playbook.dir // use playbook.dir for finding and loading require scripts
106
+ // used as cwd of command (and any scripts it requires)
107
+ build.cwd = build.cwd == null ? playbook.dir : expandPath(build.cwd, { dot: playbook.dir })
64
108
  if (!('clean' in build) && 'output' in playbook) build.clean = playbook.output.clean
65
109
  if (!('publish' in build)) build.publish = true
66
110
  if ('keepAggregateSource' in build) {
@@ -70,6 +114,14 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
70
114
  if (!build.processLimit) {
71
115
  build.processLimit = 'processLimit' in build ? Infinity : Math.round(os.cpus().length * 0.5)
72
116
  }
117
+ if ('stderr' in build) {
118
+ if (build.stderr === 'log') {
119
+ build.stderr = 'buffer'
120
+ build.stderrSink = 'log'
121
+ } else if (!['ignore', 'print'].includes(build.stderr)) {
122
+ delete build.stderr
123
+ }
124
+ }
73
125
  return config
74
126
  })
75
127
  }
@@ -86,6 +138,13 @@ function camelCaseKeys (o, stopPaths = [], p = undefined) {
86
138
  return accum
87
139
  }
88
140
 
141
+ function fileExists (path) {
142
+ return fsp.access(path).then(
143
+ () => true,
144
+ () => false
145
+ )
146
+ }
147
+
89
148
  function getLocalDate (now = new Date()) {
90
149
  return new Date(now - now.getTimezoneOffset() * 60000).toISOString().split('T')[0]
91
150
  }
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ function logCommand (loggerName, command, file, convertAttributes, attributeOptionFlagOrArgs) {
4
+ const logger = this?.getLogger(loggerName)
5
+ if (!logger?.isLevelEnabled('debug')) return
6
+ const { docfile, 'assembler-filetype': filetype } = convertAttributes
7
+ const args = Array.isArray(attributeOptionFlagOrArgs)
8
+ ? attributeOptionFlagOrArgs
9
+ : attributeOptionFlagOrArgs
10
+ ? convertAttributes.toArgs(attributeOptionFlagOrArgs, command)
11
+ : []
12
+ const ctx = { command: [command].concat(args).join(' '), file: { path: docfile } }
13
+ const msg = `Running external command to export assembly in %s to %s: %s`
14
+ const componentVersionStr = file.src.version ? `${file.src.version}@${file.src.component}` : file.src.component
15
+ logger.debug(ctx, msg, componentVersionStr, filetype, file.src.relative)
16
+ }
17
+
18
+ module.exports = logCommand