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

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,11 +11,12 @@ 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 = { true: () => true, false: () => false, new: () => ({}), void: () => undefined }
14
+ const invariably = { new: () => ({}), null: () => null, void: () => undefined }
15
15
  const PACKAGE_NAME = require('../package.json').name
16
16
  const NEWLINE_RX = /(?:\r?\n)+/
17
17
 
18
- async function assembleContent (playbook, contentCatalog, converter, { configSource, navigationCatalog }) {
18
+ async function assembleContent (playbook, contentCatalog, converter, providers) {
19
+ const { configSource, navigationCatalog, componentVersionFilter } = providers
19
20
  const {
20
21
  convert = converter,
21
22
  getDefaultCommand,
@@ -23,7 +24,8 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
23
24
  backend: targetBackend = targetExtname.substring(1),
24
25
  embedReferenceStyle = 'relative',
25
26
  mediaType: targetMediaType,
26
- loggerName = PACKAGE_NAME,
27
+ loggerName = `${PACKAGE_NAME} [${targetBackend}-exporter]`,
28
+ xmlCompliant,
27
29
  } = converter ?? {}
28
30
  const assemblerConfig = await loadConfig.call(this, configSource, playbook, '-' + targetBackend)
29
31
  if (assemblerConfig.enabled === false || !contentCatalog.publishableFamilies?.has('export')) return []
@@ -38,8 +40,9 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
38
40
  const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
39
41
  const { assembly: assemblyConfig, build: buildConfig } = assemblerConfig
40
42
  assemblyConfig.embedReferenceStyle = embedReferenceStyle
43
+ if (xmlCompliant) assemblyConfig.xmlIds ??= true
41
44
  const profile = (assemblyConfig.profile ??= targetBackend)
42
- const intrinsicAttributes = { 'loader-assembler': '' }
45
+ const intrinsicAttributes = { 'loader-assembler': '', 'assembler-root-level': assemblyConfig.rootLevel }
43
46
  const cwd = (buildConfig.cwd ??= process.cwd())
44
47
  if (profile) {
45
48
  buildConfig.dir ??= ospath.join(playbook.dir ?? process.cwd(), `build/assembler/${profile}`)
@@ -62,7 +65,8 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
62
65
  loadAsciiDoc,
63
66
  contentCatalog,
64
67
  assemblerConfig,
65
- generateSelectAssemblyProfile(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog)
68
+ generateSelectAssemblyProfile(context, contentCatalog, assemblyConfig, intrinsicAttributes, navigationCatalog),
69
+ typeof componentVersionFilter === 'function' ? componentVersionFilter : undefined
66
70
  )
67
71
  if (!(assemblyFiles.length && typeof convert === 'function')) return assemblyFiles
68
72
  buildConfig.command ??= typeof getDefaultCommand === 'function' ? await getDefaultCommand(cwd, playbook) : null
@@ -75,8 +79,7 @@ async function assembleContent (playbook, contentCatalog, converter, { configSou
75
79
  .add(
76
80
  assemblyFiles.map((doc) => async () => {
77
81
  const relativeToOutput = embedReferenceStyle === 'output-relative'
78
- const convertAttributes = prepareConvertAttributes(doc, targetExtname, relativeToOutput, assemblerConfig)
79
- if (buildConfig.mkdirs) await fsp.mkdir(convertAttributes.outdir, { recursive: true, force: true })
82
+ const convertAttributes = await prepareConvertAttributes(doc, targetExtname, relativeToOutput, assemblerConfig)
80
83
  return boundConvert(doc, convertAttributes, buildConfig, helpers).then((result) =>
81
84
  boundResolveFileOrContents(result, convertAttributes, buildConfig, loggerName).then((fileOrContents) =>
82
85
  coerceToExportFormat(doc, targetBackend, targetExtname, targetMediaType, fileOrContents)
@@ -150,29 +153,34 @@ function generateSelectAssemblyProfile (context, contentCatalog, baseModel, intr
150
153
  const attributes = Object.assign({}, baseModel.attributes)
151
154
  const model = Object.assign({}, baseModel, overrides, { attributes, logger })
152
155
  if (!overrides) return Object.assign(model, { navigation: navigation || componentVersion.navigation })
156
+ if ('rootLevel' in overrides) attributes['assembler-root-level'] = overrides.rootLevel
153
157
  delete model.navFiles
154
158
  delete model.messages
155
- if (overrides.attributes) Object.entries(overrides.attributes).forEach(([name, val]) => (attributes[name] = val))
159
+ if (overrides.attributes) {
160
+ for (const [name, val] of Object.entries(overrides.attributes)) {
161
+ if (!(name in intrinsicAttributes)) attributes[name] = val
162
+ }
163
+ }
156
164
  if (navigation) return Object.assign(model, { navigation })
157
165
  overrides.messages?.forEach(boundSendToLog)
158
166
  const navFiles = overrides.navFiles
159
167
  if (!navFiles) return Object.assign(model, { navigation: componentVersion.navigation })
160
168
  if (!navFiles.length) return Object.assign(model, { navigation: [] })
161
169
  model.navigation = buildAlternateNavigation(contentCatalog, componentVersion, navFiles, {
162
- attributes: intrinsicAttributes,
170
+ attributes: model.attributes,
163
171
  })
164
172
  return model
165
173
  }
166
174
  }
167
175
 
168
- function prepareConvertAttributes (doc, targetExtname, relativeToOutput, assemblerConfig) {
176
+ async function prepareConvertAttributes (doc, targetExtname, relativeToOutput, assemblerConfig) {
169
177
  const {
170
178
  asciidoc: { attributes: docAttributes } = { attributes: {} },
171
179
  extname: docfilesuffix,
172
180
  path: reldocfile,
173
181
  src: { family, relative },
174
182
  } = doc
175
- const { cwd = process.cwd(), dir = cwd } = assemblerConfig.build
183
+ const { cwd = process.cwd(), dir = cwd, mkdirs } = assemblerConfig.build
176
184
  const docname = family + '$' + relative.substring(0, relative.length - docfilesuffix.length)
177
185
  const docfile = ospath.join(dir, reldocfile)
178
186
  const outdir = ospath.dirname(docfile)
@@ -182,31 +190,54 @@ function prepareConvertAttributes (doc, targetExtname, relativeToOutput, assembl
182
190
  docdir,
183
191
  docfile,
184
192
  docfilesuffix,
185
- 'docname@': docname,
193
+ docname: `${docname}@`,
186
194
  imagesdir: '',
187
195
  outdir,
188
196
  outfile,
189
197
  outfilesuffix: targetExtname,
190
- toArgs (optionFlag) {
198
+ toArgs (attributeOptionFlag, outputOptionFlag) {
191
199
  const args = []
192
- for (let [name, val] of Object.entries(this)) {
193
- if (val) {
194
- val = name + '=' + val
195
- } else if (val === '') {
196
- if (name === 'asciidoctor-log-integration') {
197
- args.push('-r', require.resolve('#asciidoctor-log-adapter'))
198
- continue
199
- }
200
- val = name
200
+ let requireAsciidoctorLogAdapter
201
+ for (const [name, val] of Object.entries(this)) {
202
+ let optionValue
203
+ if (name === 'asciidoctor-log-integration') {
204
+ if ((val ?? false) !== false) requireAsciidoctorLogAdapter = val
205
+ continue
206
+ } else if ((val ?? false) === false) {
207
+ optionValue = `${name}!${val === false ? '=@' : ''}`
201
208
  } else {
202
- val = `${name}!${val === false ? '@' : ''}`
209
+ optionValue = val === '' ? name : name + '=' + val
203
210
  }
204
- args.push(optionFlag, val)
211
+ args.push(attributeOptionFlag, optionValue)
205
212
  }
213
+ if (requireAsciidoctorLogAdapter) args.push('-r', requireAsciidoctorLogAdapter)
214
+ if (outputOptionFlag) args.push(outputOptionFlag, this.outfile)
206
215
  return args
207
216
  },
208
217
  })
209
- return Object.defineProperty(attributes, 'toArgs', { enumerable: false })
218
+ Object.defineProperty(attributes, 'outfilesuffix', {
219
+ get () {
220
+ return ospath.extname(this.outfile)
221
+ },
222
+ set (value) {
223
+ const outfile = this.outfile
224
+ const extname = ospath.extname(outfile)
225
+ if (extname && outfile.endsWith(extname)) this.outfile = outfile.substring(0, outfile.length - extname.length)
226
+ this.outfile += value
227
+ },
228
+ })
229
+ Object.defineProperty(attributes, 'toArgs', { enumerable: false })
230
+ if (mkdirs) await fsp.mkdir(outdir, { recursive: true, force: true })
231
+ if (attributes['asciidoctor-log-integration'] != null) {
232
+ const scriptSourcePath = require.resolve('#asciidoctor-log-adapter')
233
+ let scriptTargetPath = scriptSourcePath
234
+ if (attributes['asciidoctor-log-integration'] === 'copy-to-build-dir') {
235
+ scriptTargetPath = ospath.join(dir, 'asciidoctor-log-adapter.rb')
236
+ await fsp.cp(scriptSourcePath, scriptTargetPath, { force: true, recursive: true })
237
+ }
238
+ attributes['asciidoctor-log-integration'] = scriptTargetPath
239
+ }
240
+ return attributes
210
241
  }
211
242
 
212
243
  function coerceToExportFormat (assemblyFile, targetBackend, targetExtname, targetMediaType, fileOrContents) {
@@ -219,7 +250,8 @@ function coerceToExportFormat (assemblyFile, targetBackend, targetExtname, targe
219
250
  const { path: sourcePath, extname: sourceExtname } = file
220
251
  const relativeWithoutExtname = file.src.relative.substring(0, file.src.relative.length - sourceExtname.length)
221
252
  const newPath = sourcePath.substring(0, sourcePath.length - sourceExtname.length) + targetExtname
222
- Object.assign(file, { mediaType: (file.src.mediaType = targetMediaType), path: newPath })
253
+ Object.assign(file, { mediaType: (file.src.mediaType = targetMediaType), path: (file.src.path = newPath) })
254
+ delete file.src.abspath
223
255
  file.src.basename = path.basename((file.src.relative = relativeWithoutExtname + (file.src.extname = targetExtname)))
224
256
  return file
225
257
  }
@@ -242,8 +274,9 @@ function prepareWorkspace (publishFiles, assemblyFiles, buildConfig) {
242
274
  }
243
275
  if (keepSource) {
244
276
  for (const file of assemblyFiles) {
245
- file.src.contents = file.contents
246
277
  file.out = { path: file.path }
278
+ file.src.contents = file.contents
279
+ file.src.abspath = ospath.join(dir, file.path)
247
280
  files.push(file)
248
281
  }
249
282
  }
@@ -254,7 +287,7 @@ function prepareWorkspace (publishFiles, assemblyFiles, buildConfig) {
254
287
  })
255
288
  }
256
289
 
257
- async function resolveFileOrContents (convertResult, convertAttributes, buildConfig, loggerName) {
290
+ function resolveFileOrContents (convertResult, convertAttributes, buildConfig, loggerName) {
258
291
  let fileOrContents
259
292
  if (convertResult?.status != null) {
260
293
  if ('file' in convertResult) {
@@ -265,12 +298,14 @@ async function resolveFileOrContents (convertResult, convertAttributes, buildCon
265
298
  fileOrContents = convertResult.stdout
266
299
  }
267
300
  let logger, match
268
- if (buildConfig.stderrSink === 'log' && convertResult.stderr.length && (logger = this.getLogger(loggerName))) {
301
+ if (buildConfig.stderrSink === 'log' && convertResult.stderr?.length && (logger = this.getLogger(loggerName))) {
269
302
  const docfile = convertAttributes.docfile
270
303
  const command = buildConfig.command
304
+ const file = { path: docfile }
271
305
  const stderr = convertResult.stderr.toString().trimEnd()
306
+ const additionalLines = ['Additional stderr lines from command:']
272
307
  stderr.split(NEWLINE_RX).forEach((line) => {
273
- const ctx = { command, file: { path: docfile } }
308
+ const ctx = { command, file }
274
309
  if (line.charAt() === '{' && line.charAt(line.length - 1) === '}') {
275
310
  const entry = JSON.parse(line)
276
311
  if (entry.name) ctx.program = entry.name
@@ -286,17 +321,17 @@ async function resolveFileOrContents (convertResult, convertAttributes, buildCon
286
321
  if (lineno) ctx.line = parseInt(lineno, 10)
287
322
  logger[level === 'WARNING' ? 'warn' : level.toLowerCase()](ctx, msg)
288
323
  } else {
289
- logger.info(ctx, line)
324
+ additionalLines.push(line)
290
325
  }
291
326
  })
327
+ if (additionalLines.length > 1) logger.info({ command, file }, additionalLines.join('\n'))
292
328
  }
293
329
  } else if (convertResult !== undefined) {
294
- return convertResult
330
+ return Promise.resolve(convertResult)
295
331
  }
296
- if (fileOrContents !== undefined) return fileOrContents
332
+ if (fileOrContents !== undefined) return Promise.resolve(fileOrContents)
297
333
  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
334
+ return fsp.access(outfile).then(() => new LazyReadable(() => fs.createReadStream(outfile)), invariably.null)
300
335
  }
301
336
 
302
337
  function isBound (obj) {
package/lib/configure.js CHANGED
@@ -7,24 +7,26 @@ function configure (context, ...args) {
7
7
  }
8
8
 
9
9
  function internalConfigure (converter, config = {}, providers = {}) {
10
- this.once('componentsRegistered', ({ contentCatalog, assemblerProfiles }) => {
11
- contentCatalog.publishableFamilies.add('export')
12
- if (assemblerProfiles) return
13
- this.updateVariables({ assemblerProfiles: getAssemblerProfiles(contentCatalog) })
14
- })
10
+ if (!this.listeners('beforeProcess').includes(enableKeepSource)) {
11
+ this.once('componentsRegistered', ({ contentCatalog, assemblerProfiles }) => {
12
+ contentCatalog.publishableFamilies.add('export')
13
+ if (!assemblerProfiles) this.updateVariables({ assemblerProfiles: getAssemblerProfiles(contentCatalog) })
14
+ })
15
15
 
16
- this.once('beforeProcess', enableKeepSource)
16
+ this.once('beforeProcess', enableKeepSource)
17
+ }
17
18
 
18
19
  this.once('navigationBuilt', async ({ playbook, contentCatalog }) => {
19
- const { assembleContent = require('./assemble-content'), ...assembleContentConfig } = providers
20
- if (assembleContentConfig.configSource?.constructor === Object) {
21
- await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfig)
20
+ const { assembleContent = require('./assemble-content'), ...assembleContentProviders } = providers
21
+ if (config.configSource?.constructor === Object) {
22
+ assembleContentProviders.configSource = config.configSource
23
+ await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentProviders)
22
24
  } else {
23
25
  const singleConfig = !('configFiles' in config)
24
26
  const configFiles = singleConfig ? config.configFile : config.configFiles
25
27
  for (const configSource of Array.isArray(configFiles) ? configFiles : [configFiles]) {
26
- const assembleContentConfigWithConfigSource = Object.assign({}, assembleContentConfig, { configSource })
27
- await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentConfigWithConfigSource)
28
+ const assembleContentProvidersWithConfigSource = Object.assign({}, assembleContentProviders, { configSource })
29
+ await assembleContent.call(this, playbook, contentCatalog, converter, assembleContentProvidersWithConfigSource)
28
30
  if (singleConfig) break
29
31
  }
30
32
  }
@@ -5,7 +5,7 @@ const { filterCollection, compilePattern } = require('./util/matcher')
5
5
  const VERSION_SEPARATOR_RX = /@(?!\()(?=(\w)?)/g
6
6
  const US = '\x1f'
7
7
 
8
- function filterComponentVersions (components, { names: patterns, prereleases = true }) {
8
+ function filterComponentVersions ({ names: patterns, prereleases = true }, components) {
9
9
  if (!patterns.length) return []
10
10
  const candidateMap = components.reduce((accum, { name: componentName, latest, versions }) => {
11
11
  for (const version of versions) {
@@ -9,6 +9,7 @@ const yaml = require('js-yaml')
9
9
  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
+ const YAML_SCHEMA = yaml.CORE_SCHEMA.withTags(yaml.mergeTag)
12
13
 
13
14
  function loadConfig (configSource, playbook, preferredQualifier = '') {
14
15
  let resolvedConfigSource
@@ -36,11 +37,11 @@ function loadConfig (configSource, playbook, preferredQualifier = '') {
36
37
  }
37
38
  return {}
38
39
  }
39
- return fsp
40
- .readFile(resolvedConfigSource)
41
- .then((data) =>
42
- Object.assign(camelCaseKeys(yaml.load(data), CAMEL_CASE_STOP_PATHS), { file: resolvedConfigSource })
43
- )
40
+ return fsp.readFile(resolvedConfigSource).then((data) =>
41
+ Object.assign(camelCaseKeys(yaml.load(data, { schema: YAML_SCHEMA }), CAMEL_CASE_STOP_PATHS), {
42
+ file: resolvedConfigSource,
43
+ })
44
+ )
44
45
  })
45
46
  ).then((config) => {
46
47
  if (config.enabled === false) return config
@@ -133,7 +134,7 @@ function loadConfig (configSource, playbook, preferredQualifier = '') {
133
134
  })
134
135
  }
135
136
 
136
- function camelCaseKeys (o, stopPaths = [], p = undefined) {
137
+ function camelCaseKeys (o, stopPaths, p = undefined) {
137
138
  if (Array.isArray(o)) return o.map((it) => camelCaseKeys(it, stopPaths, p))
138
139
  if (o == null || o.constructor !== Object) return o
139
140
  const pathPrefix = p ? p + '.' : ''
@@ -20,6 +20,7 @@ const DISCARD_ATTRIBUTE_NAMES = [
20
20
  'doctype',
21
21
  'leveloffset',
22
22
  'preface-title',
23
+ 'assembly-doctype',
23
24
  'assembly-header-attributes',
24
25
  'assembly-navtitle',
25
26
  'assembly-slug',
@@ -39,8 +40,8 @@ function produceAssemblyFile (
39
40
  assemblyModel
40
41
  ) {
41
42
  const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
42
- if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
43
43
  const pagesInOutline = selectPagesInOutline(outline, pagesByUrl)
44
+ if (outline.urlType === 'internal' && !pagesByUrl.has(outline.pathname) && !outline.items?.length) return
44
45
  const { rootLevel, xmlIds } = assemblyModel
45
46
  const idSeparators = {
46
47
  prefix:
@@ -77,6 +78,7 @@ function produceAssemblyFile (
77
78
  mutableAttributes,
78
79
  assemblyModel
79
80
  )
81
+ if (buffer[buffer.doctypeIdx] === ':doctype: article') buffer.splice(buffer.doctypeIdx, 1)
80
82
  const stem =
81
83
  (buffer.slug ? sanitizeSlug(buffer.slug) : generateSlug(rootLevel === 0 ? 'index' : buffer.navtitle)) ||
82
84
  'export-' + (assemblyModel.stemSeq = (assemblyModel.exportSeq ?? 0) + 1)
@@ -88,7 +90,7 @@ function produceAssemblyFile (
88
90
  src: { component, version, componentVersion, module: 'ROOT', family: 'export', relative: stem + '.adoc' },
89
91
  pub: false,
90
92
  })
91
- file.path = file.out.path // use out path as file path so assets can easily be published to same hierarchy
93
+ file.path = file.src.path = file.out.path // use out path as path so assets can be published to same hierarchy
92
94
  delete file.out
93
95
  return file
94
96
  }
@@ -124,7 +126,6 @@ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocCon
124
126
  }
125
127
 
126
128
  function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
127
- const doctype = assemblyModel.doctype ?? 'book'
128
129
  const navtitlePlain = sanitize(navtitle)
129
130
  const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
130
131
  const doctitle = (componentVersion.title === navtitlePlain ? '' : componentVersion.title + ': ') + navtitleAsciiDoc
@@ -133,7 +134,7 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
133
134
  const buffer = [
134
135
  `= ${doctitle}`,
135
136
  ...(version ? [`:revnumber: ${displayVersion}`] : []),
136
- ...(doctype === 'article' ? [] : [`:doctype: ${doctype}`]),
137
+ `:doctype: ${assemblyModel.doctype ?? 'book'}`,
137
138
  ':underscore: _',
138
139
  // Q: should we pass these via the CLI so they cannot be modified?
139
140
  `:page-component-name: ${componentVersion.name}`,
@@ -142,17 +143,22 @@ function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
142
143
  `:page-component-display-version: ${displayVersion}`,
143
144
  `:page-component-title: ${componentVersion.title}`,
144
145
  ]
146
+ buffer.doctypeIdx = version ? 2 : 1
145
147
  return Object.assign(buffer, { navtitle })
146
148
  }
147
149
 
148
150
  function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
149
151
  accum ??= Object.assign(new Map(), { assembled: { pages: new Map(), assets: new Set() } })
150
- const page = outlineEntry.urlType === 'internal' ? pagesByUrl.get(outlineEntry.url) : undefined
151
- if (page) {
152
- accum.set(createResourceKey(page.src), page)
153
- accum.set(outlineEntry.url, page)
152
+ const { urlType, url, hash, unresolved, items = [] } = outlineEntry
153
+ if (urlType === 'internal' && !unresolved) {
154
+ const pathname = (outlineEntry.pathname ??= hash ? url.substring(0, url.length - hash.length) : url)
155
+ let page
156
+ if (!accum.has(pathname) && (page = pagesByUrl.get(pathname))) {
157
+ accum.set(createResourceKey(page.src), page)
158
+ accum.set(pathname, page)
159
+ }
154
160
  }
155
- for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, accum)
161
+ for (const item of items) selectPagesInOutline(item, pagesByUrl, accum)
156
162
  return accum
157
163
  }
158
164
 
@@ -172,21 +178,22 @@ function mergeAsciiDoc (
172
178
  level = 0,
173
179
  supportsParts = false
174
180
  ) {
175
- const { items = [], roles = [], unresolved, urlType, url, hash } = outlineEntry
181
+ const { items = [], roles = [], unresolved, urlType, url, pathname, hash } = outlineEntry
176
182
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
177
- if (hash || roles.includes('site-only')) {
183
+ if (roles.includes('site-only')) {
178
184
  buffer.inBody ??= false
179
185
  return buffer
180
186
  }
181
- let navtitle = outlineEntry.navtitle ?? outlineEntry.content
187
+ const assembled = pagesInOutline.assembled
188
+ // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
189
+ const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(pathname) : undefined
190
+ let navtitle = outlineEntry.navtitle ?? (page && hash ? page.asciidoc.navtitle : outlineEntry.content)
182
191
  let navtitlePlain = sanitize(navtitle)
183
192
  let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
184
193
  const { filetype, linkReferenceStyle, pubRoot, sectionMergeStrategy, siteRoot, logger, rootLevel } = assemblyModel
185
- const assembled = pagesInOutline.assembled
186
- // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
187
- const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
194
+ const doctype = assemblyModel.doctype ?? 'book'
188
195
  const atDocumentRoot = !buffer.inBody
189
- const atBookRoot = atDocumentRoot && !level && assemblyModel.doctype === 'book' && (supportsParts = true)
196
+ let atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true)
190
197
  const hasItems = items.length > 0
191
198
  if (page && !assembled.pages.has(page)) {
192
199
  const contents = page.src.contents
@@ -198,6 +205,7 @@ function mergeAsciiDoc (
198
205
  Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType: page.src.mediaType })
199
206
  )
200
207
  const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
208
+ doc.catalog.syntheticIds = {}
201
209
  doc.logger = logger
202
210
  ? Object.assign(doc.getLogger().$dup(), {
203
211
  delegate: {
@@ -210,6 +218,11 @@ function mergeAsciiDoc (
210
218
  doc.source_header_attributes ??= doc.parent.$to_h()
211
219
  const isRootPage = atDocumentRoot && !level
212
220
  const { component, version, module: module_, relative, origin } = page.src
221
+ const doctypeOverride = doc.getAttribute('assembly-doctype')
222
+ if (doctypeOverride) {
223
+ if (doctypeOverride !== doctype) buffer[buffer.doctypeIdx] = `:doctype: ${doctypeOverride}`
224
+ if (doctypeOverride !== 'book') atBookRoot = supportsParts = false
225
+ }
213
226
  if (isRootPage && doc.hasAttribute('assembly-slug')) buffer.slug = doc.getAttribute('assembly-slug')
214
227
  let hasAssemblyNavtitleAttr
215
228
  if ((hasAssemblyNavtitleAttr = !!doc.getAttribute('assembly-navtitle'))) {
@@ -278,6 +291,7 @@ function mergeAsciiDoc (
278
291
  for (const entry of processDocumentHeader(doc, lines, ignoreLines, isRootPage)) {
279
292
  if (entry.type === 'author_line') {
280
293
  buffer.splice(1, 0, entry.lines[0])
294
+ buffer.doctypeIdx += 1
281
295
  buffer.endHeaderIdx += 1
282
296
  } else if (entry.type === 'attribute_entry') {
283
297
  const name = entry.name
@@ -370,6 +384,7 @@ function mergeAsciiDoc (
370
384
  // Q: should we use htitleAsciiDoc to generate ID if not {empty} instead of pageStyle
371
385
  pageFragment = `#_${idSeparators.coordinate}${pageIdLeader}${pageStyle}`
372
386
  buffer.unshift(`[#${pageId}]`)
387
+ buffer.doctypeIdx += 1
373
388
  } else {
374
389
  pageFragment = `#${pageId}`
375
390
  }
@@ -400,6 +415,7 @@ function mergeAsciiDoc (
400
415
  buffer.push(`${'='.repeat(heading.level)} ${heading.title}`)
401
416
  } else if (atDocumentRoot) {
402
417
  buffer.unshift(`[#${pageId}]`)
418
+ buffer.doctypeIdx += 1
403
419
  }
404
420
  if (sectionMergeStrategy === 'enclose' && hasItems && hasSections && !(atDocumentRoot && heading)) {
405
421
  // TODO: make overview section title configurable
@@ -475,7 +491,7 @@ function mergeAsciiDoc (
475
491
  if (~ignoreLines.indexOf(idx)) continue
476
492
  let line = lines[idx]
477
493
  if (line.startsWith('//')) {
478
- if (line[2] !== '/') continue
494
+ if (line.charAt(2) !== '/') continue
479
495
  if (line.length > 3 && line === '/'.repeat(line.length)) {
480
496
  if (skipping) {
481
497
  if (line === skipping) skipping = undefined
@@ -609,7 +625,7 @@ function mergeAsciiDoc (
609
625
  )
610
626
  if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
611
627
  }
612
- } else if (level) {
628
+ } else if (level && !hash) {
613
629
  if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title) {
614
630
  buffer.inBody ??= false
615
631
  level--
@@ -621,7 +637,7 @@ function mergeAsciiDoc (
621
637
  if (urlType === 'external') {
622
638
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
623
639
  } else if (urlType === 'internal' && !unresolved) {
624
- const resource = files.find((it) => it.pub.url === url)
640
+ const resource = files.find((it) => it.pub.url === pathname)
625
641
  if (resource) {
626
642
  if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
627
643
  const refid = generateScopedId(resource.src, componentVersion, idSeparators, filetype, true)
@@ -640,7 +656,7 @@ function mergeAsciiDoc (
640
656
  if (hasItems) {
641
657
  const nextLevel = level + 1
642
658
  // NOTE: drop first child if same as parent; should we keep if content is different?
643
- ;(urlType === 'internal' && urlType === items[0].urlType && url === items[0].url && !items[0].items
659
+ ;(urlType === 'internal' && items[0].urlType === 'internal' && pathname === items[0].url && !items[0].items?.length
644
660
  ? items.slice(1)
645
661
  : items
646
662
  ).forEach((item) => {
@@ -807,10 +823,18 @@ function safePush (onto, entries) {
807
823
 
808
824
  function generateIdFromTitle (titleAsciiDoc, idSeparators, idLeader) {
809
825
  const Section = this.$class().$const_get('::Asciidoctor::Section')
810
- const baseId = Section.$generate_id(titleAsciiDoc, this)
811
- if (idLeader) return `_${idSeparators.coordinate}${idLeader}${baseId}`
812
- this.getCatalog().refs['$[]='](baseId, true)
813
- return `_${idSeparators.coordinate}${baseId}`
826
+ const baseId = Section.$generate_id(titleAsciiDoc, this) // refs in catalog will always be empty
827
+ let id = `_${idSeparators.coordinate}${idLeader ?? ''}${baseId}`
828
+ const syntheticIds = (this.catalog.syntheticIds ??= {})
829
+ if (id in syntheticIds) {
830
+ const sep = (this.getAttribute('idseparator') ?? '_').charAt()
831
+ let cnt = this.$class().$const_get('::Asciidoctor::Compliance').unique_id_start_index
832
+ let candidate
833
+ while ((candidate = `${id}${sep}${cnt}`) in syntheticIds) cnt++
834
+ id = candidate
835
+ }
836
+ syntheticIds[id] = true
837
+ return id
814
838
  }
815
839
 
816
840
  module.exports = produceAssemblyFile
@@ -6,7 +6,13 @@ const selectMutableAttributes = require('./select-mutable-attributes')
6
6
 
7
7
  const ATTR_REF_RX = /\\?\{(\w[\w-]*)\}/g
8
8
 
9
- function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, selectAssemblyProfile) {
9
+ function produceAssemblyFiles (
10
+ loadAsciiDoc,
11
+ contentCatalog,
12
+ assemblerConfig,
13
+ selectAssemblyProfile,
14
+ selectComponentVersions
15
+ ) {
10
16
  const { assembly: assemblyConfig } = assemblerConfig
11
17
  selectAssemblyProfile ??= (componentVersion) => ({
12
18
  attributes: assemblyConfig.attributes,
@@ -26,106 +32,104 @@ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, se
26
32
  const publishableFiles = contentCatalog.getFiles().filter((file) => file.out && file.pub)
27
33
  let siteRoot
28
34
  const configMdc = assemblerConfig.file ? { file: { path: assemblerConfig.file } } : {}
29
- return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersionFilter).reduce(
30
- (accum, componentVersion) => {
31
- const assemblyModel = selectAssemblyProfile(componentVersion)
32
- const { attributes: assemblyAttributes, logger, navigation, rootLevel } = assemblyModel
33
- if (!navigation) return accum
34
- const contextualLogger = logger ? { warn: logger.warn.bind(logger, configMdc) } : undefined
35
- const { name: componentName, version, title } = componentVersion
36
- const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
37
- let sourceHighlighter
38
- if ('source-highlighter' in assemblyAttributes) {
39
- sourceHighlighter = assemblyAttributes['source-highlighter']
40
- delete assemblyAttributes['source-highlighter']
35
+ selectComponentVersions ??= filterComponentVersions.bind(null, assemblerConfig.componentVersionFilter)
36
+ return selectComponentVersions(contentCatalog.getComponents()).reduce((accum, componentVersion) => {
37
+ const assemblyModel = selectAssemblyProfile(componentVersion)
38
+ const { attributes: assemblyAttributes, logger, navigation, rootLevel } = assemblyModel
39
+ if (!navigation) return accum
40
+ const contextualLogger = logger ? { warn: logger.warn.bind(logger, configMdc) } : undefined
41
+ const { name: componentName, version, title } = componentVersion
42
+ const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
43
+ let sourceHighlighter
44
+ if ('source-highlighter' in assemblyAttributes) {
45
+ sourceHighlighter = assemblyAttributes['source-highlighter']
46
+ delete assemblyAttributes['source-highlighter']
47
+ }
48
+ const mergedAsciiDocAttributes = collateAsciiDocAttributes(
49
+ Object.assign({}, componentVersionAsciiDocConfig.attributes),
50
+ assemblyAttributes,
51
+ contextualLogger
52
+ )
53
+ const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
54
+ attributes: mergedAsciiDocAttributes,
55
+ })
56
+ assemblyModel.filetype = baseAssemblyAttributes['assembler-filetype']
57
+ const outDirname = (assemblyModel.outDirname = contentCatalog.createFile({
58
+ src: {
59
+ component: componentName,
60
+ version,
61
+ componentVersion,
62
+ module: 'ROOT',
63
+ family: 'export',
64
+ relative: 'index.adoc',
65
+ },
66
+ }).out.dirname)
67
+ assemblyModel.pubRoot = outDirname ? '/' + outDirname : ''
68
+ assemblyModel.siteRoot =
69
+ siteRoot === undefined
70
+ ? (siteRoot ??= ((val) => {
71
+ if (!val) return null
72
+ if (val.charAt(val.length - 1) === '/') val = val.substring(0, val.length - 1)
73
+ if (!val || val.charAt() === '/') return { path: val }
74
+ return { url: val, path: extractUrlPath(val) }
75
+ })(mergedAsciiDocAttributes['site-url'] || mergedAsciiDocAttributes['primary-site-url']))
76
+ : siteRoot
77
+ if (assemblyModel.filetype === 'html') {
78
+ let linkRefStyle = assemblyModel.linkReferenceStyle
79
+ if (linkRefStyle === 'absolute' && siteRoot?.url == null) linkRefStyle = 'root-relative'
80
+ if (linkRefStyle === 'root-relative' && siteRoot?.path == null) linkRefStyle = 'relative'
81
+ assemblyModel.linkReferenceStyle = linkRefStyle
82
+ } else if (!(assemblyModel.filetype === 'pdf' && assemblyModel.linkReferenceStyle === 'relative')) {
83
+ assemblyModel.linkReferenceStyle = 'absolute'
84
+ }
85
+ const rootEntry = { content: title }
86
+ let startPage =
87
+ 'startPage' in componentVersion
88
+ ? componentVersion.startPage
89
+ : contentCatalog.resolvePage('index.adoc', { component: componentName, version })
90
+ if (startPage && startPage.src.component === componentName && startPage.src.version === version) {
91
+ if (assemblyModel.insertStartPage) {
92
+ const navtitle = startPage.asciidoc?.navtitle || rootEntry.content
93
+ Object.assign(rootEntry, { navtitle, url: startPage.pub.url, urlType: 'internal', roles: ['page'] })
41
94
  }
42
- const mergedAsciiDocAttributes = collateAsciiDocAttributes(
43
- Object.assign({}, componentVersionAsciiDocConfig.attributes),
44
- assemblyAttributes,
45
- contextualLogger
46
- )
47
- const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
48
- attributes: mergedAsciiDocAttributes,
49
- })
50
- assemblyModel.filetype = baseAssemblyAttributes['assembler-filetype']
51
- const outDirname = (assemblyModel.outDirname = contentCatalog.createFile({
95
+ } else {
96
+ // Q: should we always use a reference page as startPage for computing mutableAttributes?
97
+ startPage = contentCatalog.createFile({
52
98
  src: {
53
- component: componentName,
54
- version,
99
+ component: componentVersion.name,
100
+ version: componentVersion.version,
55
101
  componentVersion,
56
102
  module: 'ROOT',
57
- family: 'export',
103
+ family: 'page',
58
104
  relative: 'index.adoc',
105
+ origin: (componentVersion.origins || [])[0],
59
106
  },
60
- }).out.dirname)
61
- assemblyModel.pubRoot = outDirname ? '/' + outDirname : ''
62
- assemblyModel.siteRoot =
63
- siteRoot === undefined
64
- ? (siteRoot ??= ((val) => {
65
- if (!val) return null
66
- if (val.charAt(val.length - 1) === '/') val = val.substring(0, val.length - 1)
67
- if (!val || val.charAt() === '/') return { path: val }
68
- return { url: val, path: extractUrlPath(val) }
69
- })(mergedAsciiDocAttributes['site-url'] || mergedAsciiDocAttributes['primary-site-url']))
70
- : siteRoot
71
- if (assemblyModel.filetype === 'html') {
72
- let linkRefStyle = assemblyModel.linkReferenceStyle
73
- if (linkRefStyle === 'absolute' && siteRoot?.url == null) linkRefStyle = 'root-relative'
74
- if (linkRefStyle === 'root-relative' && siteRoot?.path == null) linkRefStyle = 'relative'
75
- assemblyModel.linkReferenceStyle = linkRefStyle
76
- } else if (!(assemblyModel.filetype === 'pdf' && assemblyModel.linkReferenceStyle === 'relative')) {
77
- assemblyModel.linkReferenceStyle = 'absolute'
78
- }
79
- const rootEntry = { content: title }
80
- let startPage =
81
- 'startPage' in componentVersion
82
- ? componentVersion.startPage
83
- : contentCatalog.resolvePage('index.adoc', { component: componentName, version })
84
- if (startPage && startPage.src.component === componentName && startPage.src.version === version) {
85
- if (assemblyModel.insertStartPage) {
86
- const navtitle = startPage.asciidoc?.navtitle || rootEntry.content
87
- Object.assign(rootEntry, { navtitle, url: startPage.pub.url, urlType: 'internal', roles: ['page'] })
88
- }
89
- } else {
90
- // Q: should we always use a reference page as startPage for computing mutableAttributes?
91
- startPage = contentCatalog.createFile({
92
- src: {
93
- component: componentVersion.name,
94
- version: componentVersion.version,
95
- componentVersion,
96
- module: 'ROOT',
97
- family: 'page',
98
- relative: 'index.adoc',
99
- origin: (componentVersion.origins || [])[0],
100
- },
101
- })
102
- startPage.path = ['modules', startPage.src.module, startPage.src.family + 's', startPage.src.relative].join('/')
107
+ })
108
+ startPage.path = ['modules', startPage.src.module, startPage.src.family + 's', startPage.src.relative].join('/')
109
+ }
110
+ const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
111
+ prepareOutlines(navigation, rootEntry, rootLevel).reduce((any, outline) => {
112
+ const assemblyFile = produceAssemblyFile(
113
+ loadAsciiDoc,
114
+ contentCatalog,
115
+ componentVersion,
116
+ outline,
117
+ publishableFiles,
118
+ mergedAsciiDocConfig,
119
+ mutableAttributes,
120
+ assemblyModel
121
+ )
122
+ if (!assemblyFile) return any
123
+ // NOTE restore source highlighter for conversion if defined in Assembler config
124
+ if (sourceHighlighter !== undefined) {
125
+ assemblyFile.asciidoc.attributes['source-highlighter'] = sourceHighlighter
126
+ } else if (assemblyModel.filetype !== 'html') {
127
+ delete assemblyFile.asciidoc.attributes['source-highlighter']
103
128
  }
104
- const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
105
- prepareOutlines(navigation, rootEntry, rootLevel).reduce((any, outline) => {
106
- const assemblyFile = produceAssemblyFile(
107
- loadAsciiDoc,
108
- contentCatalog,
109
- componentVersion,
110
- outline,
111
- publishableFiles,
112
- mergedAsciiDocConfig,
113
- mutableAttributes,
114
- assemblyModel
115
- )
116
- if (!assemblyFile) return any
117
- // NOTE restore source highlighter for conversion if defined in Assembler config
118
- if (sourceHighlighter !== undefined) {
119
- assemblyFile.asciidoc.attributes['source-highlighter'] = sourceHighlighter
120
- } else if (assemblyModel.filetype !== 'html') {
121
- delete assemblyFile.asciidoc.attributes['source-highlighter']
122
- }
123
- return !!accum.push(assemblyFile)
124
- }, false)
125
- return accum
126
- },
127
- []
128
- )
129
+ return !!accum.push(assemblyFile)
130
+ }, false)
131
+ return accum
132
+ }, [])
129
133
  }
130
134
 
131
135
  function getAsciiDocConfigWithAsciidoctorReducerExtension (componentVersion) {
@@ -19,7 +19,7 @@ function rewriteXrefs (
19
19
  doc,
20
20
  sourceLocation
21
21
  ) {
22
- return line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|[\s\S]*?[^\\])\]/gu, (_, target, text) =>
22
+ return line.replace(/(?<![\\+])xref:((?:\.\/|:)?[\p{Alpha}0-9_/.{#].*?)\[(|[\s\S]*?[^\\])\]/gu, (_, target, text) =>
23
23
  rewriteXref(
24
24
  target,
25
25
  text,
@@ -87,7 +87,7 @@ function rewriteXref (
87
87
  const linkAttrlist = text
88
88
  ? (~text.indexOf(',') ? `"${text}"` : text) + ',role=unresolved'
89
89
  : (~target.indexOf(':') ? `${rawTarget},` : '') + 'role=unresolved'
90
- return `link:${rawTarget}[${linkAttrlist}]`
90
+ return `link:${rawTarget.charAt() === ':' ? resourceId.module + rawTarget : rawTarget}[${linkAttrlist}]`
91
91
  }
92
92
  if (fragment === resource.asciidoc.id) fragment = ''
93
93
  if (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/assembler",
3
- "version": "1.0.0-rc.4",
3
+ "version": "1.0.0-rc.6",
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)",
@@ -38,10 +38,10 @@
38
38
  "#unconvert-inline-asciidoc": "./lib/util/unconvert-inline-asciidoc.js"
39
39
  },
40
40
  "dependencies": {
41
- "@asciidoctor/reducer": "~1.1",
42
41
  "@antora/expand-path-helper": "~3.0",
43
42
  "@antora/run-command-helper": "~1.1",
44
- "js-yaml": "~4.3"
43
+ "@asciidoctor/reducer": "~1.1",
44
+ "js-yaml": "~5.2"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@antora/asciidoc-loader": "3.2.0-rc.2",