@antora/assembler 1.0.0-rc.2 → 1.0.0-rc.4

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.
@@ -11,7 +11,7 @@ const produceAssemblyFiles = require('./produce-assembly-files')
11
11
  const PromiseQueue = require('./util/promise-queue')
12
12
  const runCommand = require('@antora/run-command-helper')
13
13
 
14
- const invariably = { new: () => ({}), void: () => undefined }
14
+ const invariably = { true: () => true, false: () => false, new: () => ({}), void: () => undefined }
15
15
  const PACKAGE_NAME = require('../package.json').name
16
16
  const NEWLINE_RX = /(?:\r?\n)+/
17
17
 
@@ -25,8 +25,8 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
25
25
  mediaType: targetMediaType,
26
26
  loggerName = PACKAGE_NAME,
27
27
  } = converter ?? {}
28
- const assemblerConfig = await loadConfig.call(this, playbook, configSource, '-' + targetBackend)
29
- if (assemblerConfig.enabled === false) return []
28
+ const assemblerConfig = await loadConfig.call(this, configSource, playbook, '-' + targetBackend)
29
+ if (assemblerConfig.enabled === false || !contentCatalog.publishableFamilies?.has('export')) return []
30
30
  const context = isBound(this)
31
31
  ? this
32
32
  : {
@@ -40,7 +40,7 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
40
40
  assemblyConfig.embedReferenceStyle = embedReferenceStyle
41
41
  const profile = (assemblyConfig.profile ??= targetBackend)
42
42
  const intrinsicAttributes = { 'loader-assembler': '' }
43
- buildConfig.cwd ??= process.cwd()
43
+ const cwd = (buildConfig.cwd ??= process.cwd())
44
44
  if (profile) {
45
45
  buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler/${profile}`)
46
46
  intrinsicAttributes[`assembler-profile-${profile}`] = ''
@@ -65,35 +65,36 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
65
65
  generateSelectAssemblyProfile(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog)
66
66
  )
67
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
- }
68
+ buildConfig.command ??= typeof getDefaultCommand === 'function' ? await getDefaultCommand(cwd, playbook) : null
71
69
  const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
72
70
  await prepareWorkspace(publishFiles, assemblyFiles, buildConfig)
73
71
  const helpers = { logCommand: logCommand.bind(context, loggerName), runCommand }
74
72
  const boundConvert = convert.bind(context)
73
+ const boundResolveFileOrContents = resolveFileOrContents.bind(context)
75
74
  return new PromiseQueue({ concurrency: buildConfig.processLimit })
76
75
  .add(
77
76
  assemblyFiles.map((doc) => async () => {
78
77
  const relativeToOutput = embedReferenceStyle === 'output-relative'
79
78
  const convertAttributes = prepareConvertAttributes(doc, targetExtname, relativeToOutput, assemblerConfig)
80
79
  if (buildConfig.mkdirs) await fsp.mkdir(convertAttributes.outdir, { recursive: true, force: true })
81
- return boundConvert(doc, convertAttributes, buildConfig, helpers).then((result) => {
82
- const fileOrContents = resolveFileOrContents.call(context, result, convertAttributes, buildConfig, loggerName)
83
- return coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents)
84
- })
80
+ return boundConvert(doc, convertAttributes, buildConfig, helpers).then((result) =>
81
+ boundResolveFileOrContents(result, convertAttributes, buildConfig, loggerName).then((fileOrContents) =>
82
+ coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents)
83
+ )
84
+ )
85
85
  })
86
86
  )
87
87
  .toPromise()
88
88
  .then((files) => {
89
- if (!buildConfig.publish) {
89
+ const { publish, qualifyExports } = buildConfig
90
+ if (!publish) {
90
91
  for (const file of files) delete file.assembler.assembled
91
92
  return files
92
93
  }
93
- const qualifyExports = buildConfig.qualifyExports
94
94
  return files.map((file) => {
95
- const pages = file.assembler.assembled.pages
95
+ const pages = file.isNull() ? undefined : file.assembler.assembled.pages
96
96
  delete file.assembler.assembled
97
+ if (!pages) return file
97
98
  file = contentCatalog.addFile(file)
98
99
  const extname = file.extname
99
100
  const download = file.assembler.downloadStem + extname
@@ -208,10 +209,10 @@ function prepareConvertAttributes (doc, targetExtname, relativeToOutput, assembl
208
209
  return Object.defineProperty(attributes, 'toArgs', { enumerable: false })
209
210
  }
210
211
 
211
- function coerceToExportFormat (originalFile, targetBackend, targetExtname, targetMediaType, fileOrContents) {
212
+ function coerceToExportFormat (assemblyFile, targetBackend, targetExtname, targetMediaType, fileOrContents) {
212
213
  const file =
213
214
  fileOrContents == null || Buffer.isBuffer(fileOrContents) || typeof fileOrContents.pipe === 'function'
214
- ? Object.assign(originalFile, { contents: fileOrContents })
215
+ ? Object.assign(assemblyFile, { contents: fileOrContents })
215
216
  : fileOrContents
216
217
  ;(file.assembler ??= {}).backend = targetBackend
217
218
  if (file.extname === targetExtname) return file
@@ -253,7 +254,7 @@ function prepareWorkspace (publishFiles, assemblyFiles, buildConfig) {
253
254
  })
254
255
  }
255
256
 
256
- function resolveFileOrContents (convertResult, convertAttributes, buildConfig, loggerName) {
257
+ async function resolveFileOrContents (convertResult, convertAttributes, buildConfig, loggerName) {
257
258
  let fileOrContents
258
259
  if (convertResult?.status != null) {
259
260
  if ('file' in convertResult) {
@@ -292,9 +293,10 @@ function resolveFileOrContents (convertResult, convertAttributes, buildConfig, l
292
293
  } else if (convertResult !== undefined) {
293
294
  return convertResult
294
295
  }
295
- return fileOrContents === undefined
296
- ? new LazyReadable(() => fs.createReadStream(convertAttributes.outfile))
297
- : fileOrContents
296
+ if (fileOrContents !== undefined) return fileOrContents
297
+ const outfile = convertAttributes.outfile
298
+ const outfileExists = await fsp.access(outfile).then(invariably.true, invariably.false)
299
+ return outfileExists ? new LazyReadable(() => fs.createReadStream(outfile)) : null
298
300
  }
299
301
 
300
302
  function isBound (obj) {
@@ -10,7 +10,7 @@ const { LEGACY_ASSEMBLY_KEYS } = require('./constants')
10
10
  const CAMEL_CASE_STOP_PATHS = ['asciidoc.attributes', 'assembly.attributes']
11
11
  const PACKAGE_NAME = require('../package.json').name
12
12
 
13
- function loadConfig (playbook, configSource, preferredQualifier = '') {
13
+ function loadConfig (configSource, playbook, preferredQualifier = '') {
14
14
  let resolvedConfigSource
15
15
  return (
16
16
  configSource?.constructor === Object
@@ -52,7 +52,7 @@ function loadConfig (playbook, configSource, preferredQualifier = '') {
52
52
  }
53
53
  if (componentVersionFilter.names == null) {
54
54
  componentVersionFilter.names = ['*']
55
- } else if (typeof componentVersionFilter.names === 'string') {
55
+ } else if (componentVersionFilter.names.constructor === String) {
56
56
  componentVersionFilter.names = componentVersionFilter.names.split(', ')
57
57
  }
58
58
  const remapAssemblyKeys = !('assembly' in config)
@@ -102,7 +102,7 @@ function loadConfig (playbook, configSource, preferredQualifier = '') {
102
102
  if (command) {
103
103
  if (command.constructor !== String) {
104
104
  build.command = String(command)
105
- } else if (command.includes('$')) {
105
+ } else if (~command.indexOf('$')) {
106
106
  const vars = {
107
107
  $NODE: process.execPath,
108
108
  $NPM: ospath.join(process.execPath, '../npm'),
@@ -101,7 +101,7 @@ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocCon
101
101
  if (configShared == null) {
102
102
  const assets = pagesInOutline.assembled.assets
103
103
  for (const [name, val] of Object.entries(sharedAttributes)) {
104
- if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
104
+ if (!(val?.constructor === String && ~val.indexOf(':'))) continue
105
105
  let newVal
106
106
  if (!name.endsWith('-image') || !(newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, ctx, assets))) {
107
107
  if (~(newVal = val).indexOf('image:')) {
@@ -112,7 +112,7 @@ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocCon
112
112
  }
113
113
  }
114
114
  for (const [name, val] of Object.entries(sharedAttributes)) {
115
- if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
115
+ if (!(val?.constructor === String && ~val.indexOf(':'))) continue
116
116
  if (~val.indexOf('xref:')) {
117
117
  const newVal = rewriteXrefs(val, contentCatalog, assemblyModel, ctx, false, pagesInOutline, idSeparators)
118
118
  if (newVal !== val) (attributesModified ??= {})[name] = newVal
@@ -133,7 +133,7 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
133
133
  const buffer = [
134
134
  `= ${doctitle}`,
135
135
  ...(version ? [`:revnumber: ${displayVersion}`] : []),
136
- ...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
136
+ ...(doctype === 'article' ? [] : [`:doctype: ${doctype}`]),
137
137
  ':underscore: _',
138
138
  // Q: should we pass these via the CLI so they cannot be modified?
139
139
  `:page-component-name: ${componentVersion.name}`,
@@ -172,15 +172,15 @@ function mergeAsciiDoc (
172
172
  level = 0,
173
173
  supportsParts = false
174
174
  ) {
175
+ const { items = [], roles = [], unresolved, urlType, url, hash } = outlineEntry
175
176
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
176
- if (outlineEntry.hash) {
177
+ if (hash || roles.includes('site-only')) {
177
178
  buffer.inBody ??= false
178
179
  return buffer
179
180
  }
180
181
  let navtitle = outlineEntry.navtitle ?? outlineEntry.content
181
182
  let navtitlePlain = sanitize(navtitle)
182
183
  let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
183
- const { items = [], roles = [], unresolved, urlType, url } = outlineEntry
184
184
  const { filetype, linkReferenceStyle, pubRoot, sectionMergeStrategy, siteRoot, logger, rootLevel } = assemblyModel
185
185
  const assembled = pagesInOutline.assembled
186
186
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
@@ -194,10 +194,8 @@ function mergeAsciiDoc (
194
194
  buffer.inBody ??= false
195
195
  return buffer
196
196
  }
197
- const isRootPage = atDocumentRoot && !level
198
- const { component, version, module: module_, relative, origin, mediaType } = page.src
199
197
  const pageAsAsciiDoc = new page.constructor(
200
- Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
198
+ Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType: page.src.mediaType })
201
199
  )
202
200
  const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
203
201
  doc.logger = logger
@@ -210,6 +208,8 @@ function mergeAsciiDoc (
210
208
  })
211
209
  : doc.getLogger()
212
210
  doc.source_header_attributes ??= doc.parent.$to_h()
211
+ const isRootPage = atDocumentRoot && !level
212
+ const { component, version, module: module_, relative, origin } = page.src
213
213
  if (isRootPage && doc.hasAttribute('assembly-slug')) buffer.slug = doc.getAttribute('assembly-slug')
214
214
  let hasAssemblyNavtitleAttr
215
215
  if ((hasAssemblyNavtitleAttr = !!doc.getAttribute('assembly-navtitle'))) {
@@ -365,10 +365,10 @@ function mergeAsciiDoc (
365
365
  }
366
366
  }
367
367
  }
368
- if (htitleAsciiDoc != null) {
368
+ if (htitleAsciiDoc) {
369
369
  if (atDocumentRoot) {
370
- // NOTE use pageStyle as fallback because this section must have a unique ID
371
- pageFragment = `#${pageIdLeader}${doc.getId() ?? pageStyle}`
370
+ // Q: should we use htitleAsciiDoc to generate ID if not {empty} instead of pageStyle
371
+ pageFragment = `#_${idSeparators.coordinate}${pageIdLeader}${pageStyle}`
372
372
  buffer.unshift(`[#${pageId}]`)
373
373
  } else {
374
374
  pageFragment = `#${pageId}`
@@ -393,31 +393,30 @@ function mergeAsciiDoc (
393
393
  } else if (atBookRoot) {
394
394
  nextSectionLevel = undefined
395
395
  }
396
- let enclosed
397
396
  if (heading) {
398
397
  const rolesAttr = pageRoles.reduce((str, it) => str + '.' + it, '')
399
398
  const notitleAttrs = heading.notitle ? '%notitle,toclevels=0,outlinelevels=0' : ''
400
399
  buffer.push(`[${heading.style ?? pageStyle}${pageFragment}${rolesAttr}${notitleAttrs}]`)
401
400
  buffer.push(`${'='.repeat(heading.level)} ${heading.title}`)
402
- } else {
403
- if (atDocumentRoot) buffer.unshift(`[#${pageId}]`)
404
- if (sectionMergeStrategy === 'enclose' && hasItems && hasSections) {
405
- enclosed = true
406
- // TODO: make overview section title configurable
407
- //let overviewTitle = doc.getDocumentTitle()
408
- //if (overviewTitle === navtitle) overviewTitle = doc.getAttribute('overview-title', 'Overview')
409
- const overviewTitle = doc.getAttribute('overview-title', 'Overview')
410
- const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
411
- let hlevel = level + 2
412
- if (hlevel > 6) {
413
- const blockStyle = `discrete.h${hlevel}`
414
- hlevel = 6
415
- buffer.push(`[${blockStyle}#${syntheticId}]`)
416
- } else {
417
- buffer.push(`[#${syntheticId}]`)
418
- }
419
- buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
401
+ } else if (atDocumentRoot) {
402
+ buffer.unshift(`[#${pageId}]`)
403
+ }
404
+ if (sectionMergeStrategy === 'enclose' && hasItems && hasSections && !(atDocumentRoot && heading)) {
405
+ // TODO: make overview section title configurable
406
+ //let overviewTitle = doc.getDocumentTitle()
407
+ //if (overviewTitle === navtitle) overviewTitle = doc.getAttribute('overview-title', 'Overview')
408
+ const overviewTitle = doc.getAttribute('overview-title', 'Overview')
409
+ const syntheticId = idSeparators.generateIdFromTitle(overviewTitle, idSeparators, pageIdLeader)
410
+ if (heading) buffer.push('')
411
+ let hlevel = level + (nextSectionLevel = 2)
412
+ if (hlevel > 6) {
413
+ const blockStyle = `discrete.h${hlevel}`
414
+ hlevel = 6
415
+ buffer.push(`[${blockStyle}#${syntheticId}]`)
416
+ } else {
417
+ buffer.push(`[#${syntheticId}]`)
420
418
  }
419
+ buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
421
420
  }
422
421
  assembled.pages.set(page, pageFragment)
423
422
  if (hasSections) fixSectionLevels(doc.getSections(), nextSectionLevel)
@@ -545,7 +544,7 @@ function mergeAsciiDoc (
545
544
  if (block.getSectionName() === 'header') return
546
545
  let blockStyle = sectionMergeStrategy === 'discrete' ? 'discrete' : undefined
547
546
  lines[idx] = lines[idx].replace(/^=+ (.+)/, (_, rest) => {
548
- let targetMarkerLength = block.level + 1 + level + (enclosed ? 1 : 0)
547
+ let targetMarkerLength = block.level + 1 + level
549
548
  if (targetMarkerLength > 6) {
550
549
  blockStyle = `discrete.h${targetMarkerLength}`
551
550
  targetMarkerLength = 6
@@ -806,9 +805,10 @@ function safePush (onto, entries) {
806
805
  }
807
806
  }
808
807
 
809
- function generateIdFromTitle (titleAsciiDoc, idSeparators) {
808
+ function generateIdFromTitle (titleAsciiDoc, idSeparators, idLeader) {
810
809
  const Section = this.$class().$const_get('::Asciidoctor::Section')
811
810
  const baseId = Section.$generate_id(titleAsciiDoc, this)
811
+ if (idLeader) return `_${idSeparators.coordinate}${idLeader}${baseId}`
812
812
  this.getCatalog().refs['$[]='](baseId, true)
813
813
  return `_${idSeparators.coordinate}${baseId}`
814
814
  }
@@ -6,6 +6,7 @@ function generateScopedId (componentSrc, componentVersion, separators, filetype,
6
6
  let { component, module: mod, relative } = componentSrc
7
7
  let id = relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
8
8
  let { coordinate: coordinateSeparator, prefix: prefixSeparator } = separators
9
+ if (mod !== 'ROOT' && ~mod.indexOf('.')) mod = mod.replace(/[.]/g, '-')
9
10
  if (component !== componentVersion.name) {
10
11
  id = [component, mod === 'ROOT' ? '' : mod, id].join(coordinateSeparator)
11
12
  } else if (mod !== 'ROOT') {
@@ -8,7 +8,13 @@ class LazyReadable extends PassThrough {
8
8
  super(options)
9
9
  this._read = function () {
10
10
  delete this._read // restores original method
11
- fn.call(this, options).on('error', this.emit.bind(this, 'error')).pipe(this)
11
+ fn.call(this, options)
12
+ .on('error', (err) => {
13
+ let msg = 'Failed to create stream'
14
+ if (err.syscall === 'open') msg += ` for path '${err.path}'`
15
+ this.emit('error', new Error(msg, { cause: err }))
16
+ })
17
+ .pipe(this)
12
18
  return this._read.apply(this, arguments)
13
19
  }
14
20
  this.emit('readable')
@@ -27,7 +27,7 @@ function filterCollection (candidates, patterns, opts = {}) {
27
27
  }
28
28
 
29
29
  function compilePattern (str) {
30
- const negated = str.charAt() === '!' ? typeof (str = str.substring(1)) === 'string' : false
30
+ const negated = str.charAt() === '!' ? (str = str.substring(1)).constructor === String : false
31
31
  if (str === '**') return Object.assign(new RegExp(), { globstar: true, negated })
32
32
  if (str === '*') return Object.assign(new RegExp(), { star: true, negated })
33
33
  const buffer = { result: '', brace: undefined, group: undefined }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/assembler",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.4",
4
4
  "description": "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.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "OpenDevise Inc. (https://opendevise.com)",