@antora/assembler 1.0.0-alpha.6 → 1.0.0-alpha.8

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.
@@ -3,6 +3,7 @@
3
3
  const loadConfig = require('./load-config')
4
4
  const produceAggregateDocuments = require('./produce-aggregate-documents')
5
5
  const PromiseQueue = require('./util/promise-queue')
6
+ const runCommand = require('./util/run-command')
6
7
 
7
8
  async function assembleContent (playbook, contentCatalog, converter, { siteCatalog, configSource }) {
8
9
  // Q: could we get ContentCatalog#getComponentVersionStartPage() in Antora core?
@@ -23,7 +24,7 @@ async function assembleContent (playbook, contentCatalog, converter, { siteCatal
23
24
  // TODO: pass more information to converter so it doesn't have to compute internal stuff
24
25
  // Q: don't we need to pass in the combined/resolved AsciiDoc attributes per file or component version?
25
26
  return new PromiseQueue({ concurrency: buildConfig.processLimit })
26
- .add(aggregateDocuments.map((doc) => () => converter.call(this, doc, buildConfig)))
27
+ .add(aggregateDocuments.map((doc) => () => converter.call(this, doc, buildConfig, runCommand)))
27
28
  .toPromise()
28
29
  .then((files) => {
29
30
  if (buildConfig.publish && siteCatalog) siteCatalog.addFiles(files)
@@ -31,7 +32,7 @@ async function assembleContent (playbook, contentCatalog, converter, { siteCatal
31
32
  })
32
33
  }
33
34
 
34
- // TODO: if no workspace dir is defined; we shouldn't continue
35
+ // TODO: if no workspace dir is defined, we shouldn't continue
35
36
  function prepareWorkspace (publishFiles, aggregateDocuments, contentCatalog, buildConfig) {
36
37
  const { dir, clean, keepAggregateSource } = buildConfig
37
38
  const files = contentCatalog.findBy({ family: 'image' }).filter(({ out }) => out?.assembled)
@@ -21,11 +21,11 @@ function compilePatterns (patterns) {
21
21
  if (patterns[0].charAt() === '!') patterns = ['**', ...patterns]
22
22
  return patterns.map((pattern) => {
23
23
  const negated = pattern.charAt() === '!'
24
- if (negated) pattern = pattern.substr(1)
24
+ if (negated) pattern = pattern.slice(1)
25
25
  let version
26
26
  const separatorIdx = pattern.search(VERSION_SEPARATOR_RX)
27
27
  if (~separatorIdx) {
28
- pattern = (version = true) && `${pattern.substr(0, separatorIdx)}%${pattern.substr(separatorIdx + 1) || '*'}`
28
+ pattern = (version = true) && `${pattern.slice(0, separatorIdx)}%${pattern.slice(separatorIdx + 1) || '*'}`
29
29
  }
30
30
  return Object.assign(makePicomatchRx(pattern, PICOMATCH_OPTS), {
31
31
  globstar: pattern === '**',
@@ -1,8 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const expandPath = require('@antora/expand-path-helper')
4
- const { promises: fsp } = require('fs')
5
- const os = require('os')
4
+ const fsp = require('node:fs/promises')
5
+ const os = require('node:os')
6
6
  const yaml = require('js-yaml')
7
7
 
8
8
  function loadConfig (playbook, configSource = './antora-assembler.yml') {
@@ -27,8 +27,9 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
27
27
  config.asciidoc.attributes = asciidocAttrs = {}
28
28
  }
29
29
  if (!('doctype' in asciidocAttrs)) asciidocAttrs.doctype = 'book'
30
- asciidocAttrs.revdate = new Date().toISOString().split('T')[0]
30
+ if (!('revdate' in asciidocAttrs)) asciidocAttrs.revdate = getLocalDate()
31
31
  asciidocAttrs['page-partial'] = null
32
+ asciidocAttrs['loader-assembler'] = ''
32
33
  if (config.componentVersions == null) {
33
34
  config.componentVersions = ['*']
34
35
  } else if (typeof config.componentVersions === 'string') {
@@ -46,7 +47,7 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
46
47
  } else {
47
48
  build.dir = expandPath(build.dir || './build/assembler', { dot: playbook.dir })
48
49
  }
49
- build.cwd = playbook.dir // use playbook.dir the purpose of finding and loading require scripts
50
+ build.cwd = playbook.dir // use playbook.dir for the purpose of finding and loading require scripts
50
51
  if (!('clean' in build) && 'output' in playbook) build.clean = playbook.output.clean
51
52
  if (!('publish' in build)) build.publish = true
52
53
  if (!build.processLimit) {
@@ -62,10 +63,14 @@ function camelCaseKeys (o, stopPaths = [], p) {
62
63
  const pathPrefix = p ? p + '.' : ''
63
64
  const accum = {}
64
65
  for (const [k, v] of Object.entries(o)) {
65
- const camelKey = k.charAt() + k.substr(1).replace(/_([a-z])/g, (_, l) => l.toUpperCase())
66
+ const camelKey = k.charAt() + k.slice(1).replace(/_([a-z])/g, (_, l) => l.toUpperCase())
66
67
  accum[camelKey] = ~stopPaths.indexOf(pathPrefix + camelKey) ? v : camelCaseKeys(v, stopPaths, pathPrefix + camelKey)
67
68
  }
68
69
  return accum
69
70
  }
70
71
 
72
+ function getLocalDate (now = new Date()) {
73
+ return new Date(now - now.getTimezoneOffset() * 60000).toISOString().split('T')[0]
74
+ }
75
+
71
76
  module.exports = loadConfig
@@ -1,7 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const File = require('vinyl')
4
- const { posix: path } = require('path')
4
+ const path = require('node:path/posix')
5
+ const sanitize = require('./util/sanitize')
5
6
 
6
7
  function produceAggregateDocument (
7
8
  loadAsciiDoc,
@@ -16,7 +17,17 @@ function produceAggregateDocument (
16
17
  ) {
17
18
  const pagesInOutline = selectPagesInOutline(outline, pages)
18
19
  const navtitle = outline.content
19
- const stem = generateStem(componentVersion, navtitle)
20
+ const templateFile = contentCatalog.addFile({
21
+ src: {
22
+ component: componentVersion.name,
23
+ version: componentVersion.version,
24
+ module: 'ROOT',
25
+ family: 'page',
26
+ relative: generateSlug(navtitle),
27
+ },
28
+ })
29
+ const path_ = `${templateFile.out.path}.adoc`
30
+ contentCatalog.removeFile(templateFile)
20
31
  const header = buildAsciiDocHeader(componentVersion, navtitle, doctype)
21
32
  const body = aggregateAsciiDoc(
22
33
  loadAsciiDoc,
@@ -29,29 +40,32 @@ function produceAggregateDocument (
29
40
  mutableAttributes,
30
41
  sectionMergeStrategy
31
42
  )
32
- const relativeSrcPath = `${stem}.adoc`
33
43
  return new File({
44
+ aggregate: true,
34
45
  asciidoc: asciidocConfig,
35
46
  contents: Buffer.from([...header, ...body].join('\n') + '\n'),
36
47
  mediaType: 'text/asciidoc',
37
- path: relativeSrcPath,
48
+ path: path_,
38
49
  src: {
39
50
  component: componentVersion.name,
40
51
  version: componentVersion.version,
41
- basename: path.basename(relativeSrcPath),
42
- stem,
52
+ basename: path.basename(path_),
53
+ stem: path.basename(path_, '.adoc'),
43
54
  extname: '.adoc',
44
55
  },
45
56
  })
46
57
  }
47
58
 
48
59
  function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
49
- const doctitle = navtitle === componentVersion.title ? navtitle : `${componentVersion.title}: ${navtitle}`
60
+ const [navtitlePlain, navtitleAsciiDoc] = sanitize(navtitle)
61
+ let doctitle = navtitleAsciiDoc
62
+ if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
50
63
  const version = componentVersion.version === 'master' ? '' : componentVersion.version
51
64
  return [
52
65
  `= ${doctitle}`,
53
- ...(version ? [`v${version}`] : []),
66
+ ...(version ? [`:revnumber: ${version}`] : []),
54
67
  ...(doctype === 'article' ? [] : [`:doctype: ${doctype}`]),
68
+ ':underscore: _',
55
69
  // Q: should we pass these via the CLI so they cannot be modified?
56
70
  `:page-component-name: ${componentVersion.name}`,
57
71
  `:page-component-version:${version ? ' ' + version : ''}`,
@@ -84,16 +98,24 @@ function aggregateAsciiDoc (
84
98
  asciidocConfig,
85
99
  mutableAttributes,
86
100
  sectionMergeStrategy,
101
+ lastComponentVersion = componentVersion,
87
102
  level = 0
88
103
  ) {
89
104
  const buffer = []
90
105
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
91
106
  if (outlineEntry.hash) return buffer
92
107
  const { content: navtitle, items, unresolved, urlType, url } = outlineEntry
108
+ const [navtitlePlain, navtitleAsciiDoc, navtitlePass] = sanitize(navtitle)
109
+ const siteUrl = ((val) => {
110
+ if (!val || val === '/') return ''
111
+ return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
112
+ })(asciidocConfig.attributes['site-url'])
93
113
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
94
114
  let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
115
+ if (page && pagesInOutline.aggregated?.includes(page)) page = undefined
95
116
  if (page) {
96
117
  let contents = page.src.contents
118
+ if (contents == null) return buffer
97
119
  // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
98
120
  // IMPORTANT: this must not leave behind lines the parser will drop!
99
121
  // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
@@ -103,17 +125,36 @@ function aggregateAsciiDoc (
103
125
  .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
104
126
  .trimRight()
105
127
  )
128
+ ;(pagesInOutline.aggregated ??= []).push(page)
106
129
  page = new page.constructor(Object.assign({}, page, { contents, mediaType: 'text/asciidoc' }))
107
- const { module: module_, relative, origin } = page.src
130
+ const { component, version, module: module_, relative, origin } = page.src
108
131
  const doc = loadAsciiDoc(page, contentCatalog, asciidocConfig)
109
132
  const refs = doc.getCatalog().refs
110
133
  // NOTE: in Antora, docname is relative src path from module without file extension
111
134
  const docname = doc.getAttribute('docname')
112
- //const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docname}//`
113
- const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docname.replace(/[/]/g, '::')}:::`
114
- //const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docname.replace(/[/]/g, ':-:')}:--:`
135
+ const docnameForId = docname.replace(/[/]/g, '::').replace(/[.]/g, '-')
136
+ const scopeId = component !== componentVersion.name
137
+ const idprefix =
138
+ (scopeId ? component + ':' : '') +
139
+ (module_ === 'ROOT' ? (scopeId ? ':' : '') : module_ + ':') +
140
+ docnameForId +
141
+ ':::'
115
142
  buffer.push('')
116
143
  buffer.push(`:docname: ${docname}`)
144
+ if (component !== lastComponentVersion.name) {
145
+ const thisComponentVersion =
146
+ component === componentVersion.name && version === componentVersion.version
147
+ ? componentVersion
148
+ : contentCatalog.getComponentVersion(component, version)
149
+ if (thisComponentVersion) {
150
+ buffer.push(`:page-component-name: ${thisComponentVersion.name}`)
151
+ buffer.push(`:page-component-version:${thisComponentVersion.version ? ' ' + thisComponentVersion.version : ''}`)
152
+ buffer.push(':page-version: {page-component-version}')
153
+ buffer.push(`:page-component-display-version: ${thisComponentVersion.displayVersion}`)
154
+ buffer.push(`:page-component-title: ${thisComponentVersion.title}`)
155
+ lastComponentVersion = thisComponentVersion
156
+ }
157
+ }
117
158
  buffer.push(`:page-module: ${module_}`)
118
159
  buffer.push(`:page-relative-src-path: ${relative}`)
119
160
  //buffer.push(`:page-origin-type: ${origin.type}`)
@@ -125,7 +166,7 @@ function aggregateAsciiDoc (
125
166
  let enclosed
126
167
  // NOTE: if level is 0, doctitle has already been added and we're in the document header
127
168
  if (level) {
128
- if (level === 1 && navtitle === componentVersion.title) {
169
+ if (level === 1 && navtitlePlain === componentVersion.title) {
129
170
  level--
130
171
  } else {
131
172
  let hlevel = level + 1
@@ -135,7 +176,7 @@ function aggregateAsciiDoc (
135
176
  } else {
136
177
  buffer.push(`[#${idprefix}]`)
137
178
  }
138
- buffer.push(`${'='.repeat(hlevel)} ${navtitle}`)
179
+ buffer.push(`${'='.repeat(hlevel)} ${navtitleAsciiDoc}`)
139
180
  }
140
181
  } else {
141
182
  header.unshift(`[#${idprefix}]`)
@@ -167,10 +208,6 @@ function aggregateAsciiDoc (
167
208
  buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
168
209
  if (toggleSectids) buffer.push(':sectids:')
169
210
  }
170
- const siteUrl = ((val) => {
171
- if (!val || val === '/') return ''
172
- return val.charAt(val.length - 1) === '/' ? val.substr(0, val.length - 1) : val
173
- })(doc.getAttribute('site-url'))
174
211
  const lines = doc.getSourceLines()
175
212
  const ignoreLines = []
176
213
  // TODO: think more about when multipart is allowed; perhaps configurable
@@ -206,9 +243,23 @@ function aggregateAsciiDoc (
206
243
  for (let i = idx; i < block.lines.length + (delimited ? idx + 2 : idx); i++) ignoreLines.push(i)
207
244
  }
208
245
  })
246
+ let skipping
209
247
  for (let idx = 0, len = lines.length; idx < len; idx++) {
210
248
  if (~ignoreLines.indexOf(idx)) continue
211
249
  let line = lines[idx]
250
+ if (line.startsWith('//')) {
251
+ if (line[2] !== '/') continue
252
+ if (line.length > 3 && line === '/'.repeat(line.length)) {
253
+ if (skipping) {
254
+ if (line === skipping) skipping = undefined
255
+ } else {
256
+ skipping = line
257
+ }
258
+ continue
259
+ }
260
+ } else if (skipping) {
261
+ continue
262
+ }
212
263
  if (line.charAt() === ':' && /^:(?:leveloffset: .*|!leveloffset:|leveloffset!:)$/.test(line)) {
213
264
  if (lines[idx - 1] === '') lines[idx - 1] = undefined
214
265
  lines[idx] = undefined
@@ -233,8 +284,8 @@ function aggregateAsciiDoc (
233
284
  let pagePart, fragment, targetPage
234
285
  const hashIdx = target.indexOf('#')
235
286
  if (~hashIdx) {
236
- pagePart = target.substr(0, hashIdx)
237
- fragment = target.substr(hashIdx + 1)
287
+ pagePart = target.slice(0, hashIdx)
288
+ fragment = target.slice(hashIdx + 1)
238
289
  // TODO: for now, assume .adoc; in the future, consider other file extensions
239
290
  if (pagePart && !pagePart.endsWith('.adoc')) pagePart += '.adoc'
240
291
  } else if (target.endsWith('.adoc')) {
@@ -250,22 +301,29 @@ function aggregateAsciiDoc (
250
301
  : `<<${idprefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
251
302
  }
252
303
  if (~pagePart.indexOf('@') || /:.*:/.test(pagePart)) {
304
+ if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
305
+ text ||= targetPage.asciidoc?.xreftext || target
306
+ return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
307
+ }
253
308
  // TODO: handle unresolved page better
254
- return siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src))
255
- ? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
256
- : m
309
+ return m
257
310
  } else if (pagePart.indexOf(':') < 0) {
258
311
  if (module_ !== 'ROOT') pagePart = `${module_}:${pagePart}`
259
312
  } else if (pagePart.startsWith('ROOT:')) {
260
- pagePart = pagePart.substr(5)
313
+ pagePart = pagePart.slice(5)
261
314
  }
262
315
  if (!(targetPage = pagesInOutline.get(pagePart))) {
316
+ if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
317
+ text ||= targetPage.asciidoc?.xreftext || target
318
+ return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
319
+ }
263
320
  // TODO: handle unresolved page better
264
- return siteUrl && (targetPage = contentCatalog.resolvePage(target, page.src))
265
- ? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
266
- : m
321
+ return m
267
322
  }
268
- pagePart = pagePart.replace(/[/]/g, '::').replace(/\.adoc$/, '')
323
+ pagePart = pagePart
324
+ .replace(/[/]/g, '::')
325
+ .replace(/\.adoc$/, '')
326
+ .replace(/[.]/g, '-')
269
327
  const refid = `${pagePart}:::${fragment}`
270
328
  return `<<${refid}${text && text !== targetPage.title ? ',' + text.replace(/\\]/g, ']') : ''}>>`
271
329
  })
@@ -281,7 +339,10 @@ function aggregateAsciiDoc (
281
339
  family: 'attachment',
282
340
  relative,
283
341
  })
284
- return attachment ? `${siteUrl}${attachment.pub.url}[${text}]` : m
342
+ // TODO: handle unresolved attachment page
343
+ return attachment && attachment.out
344
+ ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]`
345
+ : m
285
346
  })
286
347
  }
287
348
  if (~line.indexOf('image:') && !line.startsWith('image::')) {
@@ -289,9 +350,9 @@ function aggregateAsciiDoc (
289
350
  if (isResourceSpec(target)) {
290
351
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
291
352
  // TODO: handle (or report) unresolved image better
292
- if (image) {
353
+ if (image && image.out) {
293
354
  image.out.assembled = true
294
- return `image:${image.pub.url.substr(1)}[${attrlist}]`
355
+ return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
295
356
  }
296
357
  }
297
358
  return m
@@ -331,14 +392,14 @@ function aggregateAsciiDoc (
331
392
  let prefix = ''
332
393
  // Q: can we use startsWith('image::') in certain cases?
333
394
  let imageMacroOffset = (
334
- lastImageMacroAt?.[0] === idx ? line.substr(0, lastImageMacroAt[1]) : line
395
+ lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line
335
396
  ).lastIndexOf('image::')
336
397
  if (imageMacroOffset > 0) {
337
398
  if (
338
399
  block.getDocument().isNested() &&
339
- (prefix = line.substr(0, imageMacroOffset)).trimRight().endsWith('|')
400
+ (prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|')
340
401
  ) {
341
- line = line.substr(prefix.length)
402
+ line = line.slice(prefix.length)
342
403
  } else {
343
404
  imageMacroOffset = -1
344
405
  }
@@ -348,9 +409,10 @@ function aggregateAsciiDoc (
348
409
  if (isResourceSpec(target)) {
349
410
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
350
411
  // FIXME: handle (or report) case when image is not resolved
351
- if (image) {
352
- lines[idx] = `${prefix}image::${image.pub.url.substr(1)}${line.substr(line.indexOf('['))}`
412
+ if (image && image.out) {
413
+ const boxedAttrlist = line.slice(line.indexOf('['))
353
414
  image.out.assembled = true
415
+ lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
354
416
  }
355
417
  }
356
418
  lastImageMacroAt = [idx, imageMacroOffset]
@@ -375,7 +437,15 @@ function aggregateAsciiDoc (
375
437
  } else if (val !== initialVal) {
376
438
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
377
439
  }
378
- } else if (val != null && !(doc.isAttributeLocked(name) || name === 'doctype' || name === 'leveloffset')) {
440
+ } else if (
441
+ !(
442
+ val == null ||
443
+ doc.isAttributeLocked(name) ||
444
+ name === 'doctype' ||
445
+ name === 'leveloffset' ||
446
+ name === 'underscore'
447
+ )
448
+ ) {
379
449
  accum.push(`:!${name}:`)
380
450
  }
381
451
  return accum
@@ -384,43 +454,54 @@ function aggregateAsciiDoc (
384
454
  )
385
455
  if (resolvedAttributeEntries.length > 1) buffer.push(...resolvedAttributeEntries)
386
456
  }
387
- } else {
388
- if (level) {
389
- if (level === 1 && navtitle === componentVersion.title) {
390
- level--
391
- } else {
392
- buffer.push('')
393
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
394
- let toggleSectids, syntheticId
395
- if (!('sectids' in asciidocConfig.attributes)) {
457
+ } else if (level) {
458
+ if (level === 1 && navtitlePlain === componentVersion.title) {
459
+ level--
460
+ } else {
461
+ buffer.push('')
462
+ // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
463
+ // Q: should we unset docname, page-module, etc?
464
+ let toggleSectids, syntheticId
465
+ if (!('sectids' in asciidocConfig.attributes)) {
466
+ buffer.push(':!sectids:')
467
+ toggleSectids = true
468
+ } else if (typeof asciidocConfig.attributes.sectids === 'string') {
469
+ if ('sectids' in mutableAttributes) {
396
470
  buffer.push(':!sectids:')
397
471
  toggleSectids = true
398
- } else if (typeof asciidocConfig.attributes.sectids === 'string') {
399
- if ('sectids' in mutableAttributes) {
400
- buffer.push(':!sectids:')
401
- toggleSectids = true
402
- } else {
403
- syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
404
- }
472
+ } else {
473
+ syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
405
474
  }
406
- // Q: should we unset docname, page-module, etc?
407
- const sectionTitle = urlType === 'external' ? `${url}[${navtitle.replace(/\]/, '\\]')}]` : navtitle
408
- let hlevel = level + 1
409
- if (hlevel > 6) {
410
- hlevel = 6
411
- buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
412
- } else if (syntheticId) {
413
- buffer.push(`[#${syntheticId}]`)
475
+ }
476
+ let sectionTitle = navtitleAsciiDoc
477
+ if (urlType === 'external') {
478
+ sectionTitle = `${url}[${navtitlePass ? navtitleAsciiDoc : navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
479
+ } else if (urlType === 'internal' && !unresolved && siteUrl) {
480
+ const resource = contentCatalog.getFiles().find((it) => it.pub.url === url)
481
+ if (resource?.out) {
482
+ sectionTitle = navtitlePass ? navtitleAsciiDoc : navtitleAsciiDoc.replace(/\]/g, '\\]')
483
+ sectionTitle = `${siteUrl}${resource.pub.url}[${sectionTitle}]`
414
484
  }
415
- buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
416
- if (toggleSectids) buffer.push(':sectids:')
417
485
  }
486
+ let hlevel = level + 1
487
+ if (hlevel > 6) {
488
+ hlevel = 6
489
+ buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
490
+ } else if (syntheticId) {
491
+ buffer.push(`[#${syntheticId}]`)
492
+ }
493
+ buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
494
+ if (toggleSectids) buffer.push(':sectids:')
418
495
  }
419
496
  }
420
497
 
421
498
  const nextLevel = level + 1
422
499
  if (items) {
423
- items.forEach((item) => {
500
+ // NOTE: drop first child if same as parent; should we keep if content is different?
501
+ ;(urlType === 'internal' && urlType === items[0].urlType && url === items[0].url && !items[0].items
502
+ ? items.slice(1)
503
+ : items
504
+ ).forEach((item) => {
424
505
  buffer.push(
425
506
  ...aggregateAsciiDoc(
426
507
  loadAsciiDoc,
@@ -432,6 +513,7 @@ function aggregateAsciiDoc (
432
513
  asciidocConfig,
433
514
  mutableAttributes,
434
515
  sectionMergeStrategy,
516
+ lastComponentVersion,
435
517
  nextLevel
436
518
  )
437
519
  )
@@ -440,19 +522,12 @@ function aggregateAsciiDoc (
440
522
  return buffer
441
523
  }
442
524
 
443
- function generateStem (componentVersion, title) {
444
- const { name, version } = componentVersion
445
- const segments = []
446
- if (name !== 'ROOT') segments.push(name)
447
- if (version && version !== 'master') segments.push(version)
448
- segments.push(
449
- title
450
- .toLowerCase()
451
- .replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '')
452
- .replace(/[ _.]/g, '-')
453
- .replace(/--+/g, '-')
454
- )
455
- return path.join(...segments)
525
+ function generateSlug (title) {
526
+ return title
527
+ .toLowerCase()
528
+ .replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '')
529
+ .replace(/[ _.]/g, '-')
530
+ .replace(/--+/g, '-')
456
531
  }
457
532
 
458
533
  function fixSectionLevels (sections, multipart) {
@@ -489,10 +564,10 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
489
564
  let rawStyle
490
565
  const commaIdx = prevLine.indexOf(',')
491
566
  if (~commaIdx) {
492
- rawStyle = prevLine.substr(1, commaIdx - 1)
567
+ rawStyle = prevLine.slice(1, commaIdx - 1)
493
568
  if (~rawStyle.indexOf('=')) rawStyle = undefined
494
569
  } else if (!~prevLine.indexOf('=')) {
495
- rawStyle = prevLine.substr(1, prevLine.length - 2)
570
+ rawStyle = prevLine.slice(1, prevLine.length - 2)
496
571
  }
497
572
  if (rawStyle) {
498
573
  if (~rawStyle.indexOf('#')) {
@@ -500,11 +575,11 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
500
575
  if (replacementStyle) prevLine = prevLine.replace(/^[^#.%,\]]+/, `[${replacementStyle}`)
501
576
  } else {
502
577
  prevLine = `[${
503
- replacementStyle ? rawStyle.replace(/^[^.%]*]/, replacementStyle) : rawStyle
504
- }#${idprefix}${block.getId()}${prevLine.substr(rawStyle.length + 1)}`
578
+ replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle
579
+ }#${idprefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
505
580
  }
506
581
  } else {
507
- prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.substr(1)}`
582
+ prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
508
583
  }
509
584
  if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
510
585
  lines[idx - 1] = prevLine
@@ -1,14 +1,18 @@
1
1
  'use strict'
2
2
 
3
+ const File = require('vinyl')
3
4
  const filterComponentVersions = require('./filter-component-versions')
4
5
  const produceAggregateDocument = require('./produce-aggregate-document')
5
6
  const selectMutableAttributes = require('./select-mutable-attributes')
6
7
 
8
+ const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
9
+
7
10
  function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfig) {
8
11
  const { insertStartPage, rootLevel, sectionMergeStrategy, asciidoc: assemblerAsciiDocConfig } = assemblerConfig
9
12
  const assemblerAsciiDocAttributes = Object.assign({}, assemblerAsciiDocConfig.attributes)
10
- const { doctype, 'source-highlighter': sourceHighlighter } = assemblerAsciiDocAttributes
13
+ const { doctype, revdate, 'source-highlighter': sourceHighlighter } = assemblerAsciiDocAttributes
11
14
  delete assemblerAsciiDocAttributes.doctype
15
+ delete assemblerAsciiDocAttributes.revdate
12
16
  delete assemblerAsciiDocAttributes['source-highlighter']
13
17
  return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersions).reduce(
14
18
  (accum, componentVersion) => {
@@ -16,22 +20,34 @@ function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfi
16
20
  if (!navigation) return accum
17
21
  const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
18
22
  const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
19
- attributes: Object.assign({}, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
23
+ attributes: Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
24
+ })
25
+ const mergedAsciiDocAttributes = mergedAsciiDocConfig.attributes
26
+ Object.entries(mergedAsciiDocAttributes).forEach(([name, val]) => {
27
+ const match = name.endsWith('-image') && val.startsWith('image:') && IMAGE_MACRO_RX.exec(val)
28
+ if (!(match && isResourceRef(match[1]))) return
29
+ // Q should we allow image to be resolved relative to component version?
30
+ const image = contentCatalog.resolveResource(match[1], undefined, 'image', ['image'])
31
+ if (!image?.out) return
32
+ mergedAsciiDocAttributes[name] = `image:${image.out.path}[${match[2]}]`
33
+ image.out.assembled = true
20
34
  })
21
35
  const rootEntry = { content: title }
22
- const startPage = contentCatalog.getComponentVersionStartPage(componentName, version)
23
- let startPageUrl
24
- if (startPage && insertStartPage) {
25
- if (includedInNav(navigation, (startPageUrl = startPage.pub.url))) {
26
- startPageUrl = undefined
27
- } else {
28
- Object.assign(rootEntry, { url: startPageUrl, urlType: 'internal' })
36
+ let startPage = contentCatalog.getComponentVersionStartPage(componentName, version)
37
+ if (startPage && startPage.src.component === componentName && startPage.src.version === version) {
38
+ if (insertStartPage && !includedInNav(navigation, startPage.pub.url)) {
39
+ Object.assign(rootEntry, { url: startPage.pub.url, urlType: 'internal' })
29
40
  }
41
+ } else {
42
+ // Q: should we always use a reference page as startPage for computing mutableAttributes?
43
+ startPage = createFile({
44
+ component: componentVersion.name,
45
+ version: componentVersion.version,
46
+ relative: '.reference-page.adoc',
47
+ origin: (componentVersion.origins || [])[0],
48
+ })
30
49
  }
31
- // Q: should we use an artificial page instead or as fallback?
32
- const mutableAttributes = startPage
33
- ? selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
34
- : {}
50
+ const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
35
51
  delete mutableAttributes.doctype
36
52
  accum = accum.concat(
37
53
  prepareOutlines(navigation, rootEntry, rootLevel).map((outline) =>
@@ -48,16 +64,40 @@ function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfi
48
64
  )
49
65
  )
50
66
  )
51
- mergedAsciiDocConfig.attributes.doctype = doctype
67
+ mergedAsciiDocAttributes.doctype = doctype
52
68
  sourceHighlighter
53
- ? (mergedAsciiDocConfig.attributes['source-highlighter'] = sourceHighlighter)
54
- : delete mergedAsciiDocConfig.attributes['source-highlighter']
69
+ ? (mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter)
70
+ : delete mergedAsciiDocAttributes['source-highlighter']
55
71
  return accum
56
72
  },
57
73
  []
58
74
  )
59
75
  }
60
76
 
77
+ function createFile (src) {
78
+ const familySegment = (src.family ??= 'page') + 's'
79
+ const path = `modules/${(src.module ??= 'ROOT')}/${familySegment}/${src.relative}`
80
+ const moduleRootPath = Array(src.relative.split('/').length - 1)
81
+ .fill('..')
82
+ .join('/')
83
+ const outPath = [
84
+ src.component === 'ROOT' ? '' : src.component,
85
+ src.version,
86
+ src.module === 'ROOT' ? '' : src.module,
87
+ src.family === 'page' ? '' : '_' + familySegment,
88
+ src.family === 'page' ? src.relative.replace(/\.adoc$/, '.html') : src.relative,
89
+ ]
90
+ .filter((it) => it)
91
+ .join('/')
92
+ return new File({
93
+ path,
94
+ contents: src.contents ?? Buffer.alloc(0),
95
+ src,
96
+ out: { path: outPath },
97
+ pub: { url: '/' + outPath, moduleRootPath },
98
+ })
99
+ }
100
+
61
101
  function getAsciiDocConfigWithAsciidoctorReducerExtension (componentVersion) {
62
102
  const asciidoctorReducerExtension = require('./asciidoctor/reducer-extension') // NOTE: must be required lazily
63
103
  const asciidocConfig = componentVersion.asciidoc
@@ -81,13 +121,20 @@ function includedInNav (items, url) {
81
121
  return items.find((it) => it.url === url || includedInNav(it.items || [], url))
82
122
  }
83
123
 
124
+ function isResourceRef (target) {
125
+ return ~target.indexOf(':') && !(~target.indexOf('://') || (target.startsWith('data:') && ~target.indexOf(',')))
126
+ }
127
+
128
+ // when root level is 0, merge the navigation into the rootEntry
129
+ // when root level is 1, create navigation per navigation menu
130
+ // in this case, if there's only a single navigation menu with no title, promote each top-level item to a menu
84
131
  function prepareOutlines (navigation, rootEntry, rootLevel) {
85
132
  if (rootLevel === 0 || navigation.length === 1) {
86
133
  const navBranch =
87
134
  navigation.length === 1
88
135
  ? navigation[0]
89
136
  : { items: navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), []) }
90
- return [Object.assign(rootEntry, navBranch)]
137
+ return rootLevel === 0 || navBranch.content ? [Object.assign(rootEntry, navBranch)] : navBranch.items
91
138
  }
92
139
  return navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), [rootEntry])
93
140
  }
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { PassThrough } = require('stream')
3
+ const { PassThrough } = require('node:stream')
4
4
 
5
5
  // adapted from https://github.com/jpommerening/node-lazystream/blob/master/lib/lazystream.js | license: MIT
6
6
  class LazyReadable extends PassThrough {
@@ -1,8 +1,8 @@
1
1
  'use strict'
2
2
 
3
- const fs = require('fs')
3
+ const fs = require('node:fs')
4
4
  const LazyReadable = require('./lazy-readable')
5
- const { spawn } = require('child_process')
5
+ const { spawn } = require('node:child_process')
6
6
 
7
7
  const IS_WIN = process.platform === 'win32'
8
8
  const DBL_QUOTE_RX = /"/g
@@ -0,0 +1,15 @@
1
+ 'use strict'
2
+
3
+ const XML_TAG_RX = /<[^>]+>/g
4
+ const XML_SPECIAL_CHARS = { '&lt;': '<', '&gt;': '>', '&amp;': '&' }
5
+ const XML_SPECIAL_CHARS_RX = /&(?:[lg]t|amp);/g
6
+
7
+ function sanitize (str) {
8
+ return ~str.indexOf('<')
9
+ ? [str.replace(XML_TAG_RX, '').replace(XML_SPECIAL_CHARS_RX, (m) => XML_SPECIAL_CHARS[m]), `+++${str}+++`, true]
10
+ : ~str.indexOf('&')
11
+ ? Array(2).fill(str.replace(XML_SPECIAL_CHARS_RX, (m) => XML_SPECIAL_CHARS[m]))
12
+ : [str, str]
13
+ }
14
+
15
+ module.exports = sanitize
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/assembler",
3
- "version": "1.0.0-alpha.6",
3
+ "version": "1.0.0-alpha.8",
4
4
  "description": "An extension library for Antora that assembles content from multiple pages into a single AsciiDoc file to converted and publish.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "OpenDevise Inc. (https://opendevise.com)",
@@ -15,8 +15,8 @@
15
15
  },
16
16
  "scripts": {
17
17
  "test": "_mocha test",
18
- "prepublishOnly": "node $npm_config_local_prefix/npm/prepublishOnly.js",
19
- "postpublish": "node $npm_config_local_prefix/npm/postpublish.js"
18
+ "prepublishOnly": "npx -y downdoc --prepublish",
19
+ "postpublish": "npx -y downdoc --postpublish"
20
20
  },
21
21
  "main": "lib/index.js",
22
22
  "exports": {
@@ -31,13 +31,13 @@
31
31
  "dependencies": {
32
32
  "@antora/expand-path-helper": "~2.0",
33
33
  "braces": "~3.0",
34
- "picomatch": "~2.3",
34
+ "picomatch": "~3.0",
35
35
  "vinyl": "~2.2",
36
36
  "js-yaml": "~4.1"
37
37
  },
38
38
  "devDependencies": {
39
- "@antora/asciidoc-loader": "3.0.1",
40
- "@antora/site-publisher": "3.0.1"
39
+ "@antora/asciidoc-loader": "~3.1",
40
+ "@antora/site-publisher": "~3.1"
41
41
  },
42
42
  "engines": {
43
43
  "node": ">=16.0.0"