@antora/assembler 1.0.0-alpha.7 → 1.0.0-alpha.9

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,8 +3,9 @@
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
- async function assembleContent (playbook, contentCatalog, converter, { siteCatalog, configSource }) {
8
+ async function assembleContent (playbook, contentCatalog, convertDocument, { siteCatalog, configSource }) {
8
9
  // Q: could we get ContentCatalog#getComponentVersionStartPage() in Antora core?
9
10
  if (typeof contentCatalog.getComponentVersionStartPage !== 'function') {
10
11
  contentCatalog.getComponentVersionStartPage = function (component, version) {
@@ -16,14 +17,14 @@ async function assembleContent (playbook, contentCatalog, converter, { siteCatal
16
17
  const generatorFunctions = this ? this.getFunctions() : {}
17
18
  const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
18
19
  const aggregateDocuments = produceAggregateDocuments(loadAsciiDoc, contentCatalog, assemblerConfig)
19
- if (!converter) return aggregateDocuments
20
+ if (!convertDocument) return aggregateDocuments
20
21
  const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
21
22
  const buildConfig = assemblerConfig.build
22
23
  await prepareWorkspace(publishFiles, aggregateDocuments, contentCatalog, buildConfig)
23
- // TODO: pass more information to converter so it doesn't have to compute internal stuff
24
+ // TODO: pass more information to convertDocument 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) => () => convertDocument.call(this, doc, buildConfig, runCommand)))
27
28
  .toPromise()
28
29
  .then((files) => {
29
30
  if (buildConfig.publish && siteCatalog) siteCatalog.addFiles(files)
@@ -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 === '**',
@@ -29,6 +29,7 @@ function loadConfig (playbook, configSource = './antora-assembler.yml') {
29
29
  if (!('doctype' in asciidocAttrs)) asciidocAttrs.doctype = 'book'
30
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,7 +63,7 @@ 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
@@ -2,6 +2,8 @@
2
2
 
3
3
  const File = require('vinyl')
4
4
  const path = require('node:path/posix')
5
+ const sanitize = require('./util/sanitize')
6
+ const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
5
7
 
6
8
  function produceAggregateDocument (
7
9
  loadAsciiDoc,
@@ -14,12 +16,20 @@ function produceAggregateDocument (
14
16
  mutableAttributes,
15
17
  sectionMergeStrategy = 'discrete'
16
18
  ) {
17
- const pagesInOutline = selectPagesInOutline(
18
- outline,
19
- pages.filter((it) => it.out)
20
- )
19
+ const pagesInOutline = selectPagesInOutline(outline, pages)
21
20
  const navtitle = outline.content
22
- const stem = generateStem(componentVersion, navtitle)
21
+ const templateFile = contentCatalog.addFile({
22
+ src: {
23
+ component: componentVersion.name,
24
+ version: componentVersion.version,
25
+ module: 'ROOT',
26
+ family: 'page',
27
+ relative: generateSlug(navtitle),
28
+ },
29
+ })
30
+ const { dir: outDir, name: outName } = path.parse(templateFile.out.path)
31
+ const path_ = outName === templateFile.src.relative ? path.join(outDir, outName + '.adoc') : outDir + '.adoc'
32
+ contentCatalog.removeFile(templateFile)
23
33
  const header = buildAsciiDocHeader(componentVersion, navtitle, doctype)
24
34
  const body = aggregateAsciiDoc(
25
35
  loadAsciiDoc,
@@ -32,24 +42,27 @@ function produceAggregateDocument (
32
42
  mutableAttributes,
33
43
  sectionMergeStrategy
34
44
  )
35
- const relativeSrcPath = `${stem}.adoc`
36
45
  return new File({
46
+ aggregate: true,
37
47
  asciidoc: asciidocConfig,
38
48
  contents: Buffer.from([...header, ...body].join('\n') + '\n'),
39
49
  mediaType: 'text/asciidoc',
40
- path: relativeSrcPath,
50
+ path: path_,
41
51
  src: {
42
52
  component: componentVersion.name,
43
53
  version: componentVersion.version,
44
- basename: path.basename(relativeSrcPath),
45
- stem,
54
+ basename: path.basename(path_),
55
+ stem: path.basename(path_, '.adoc'),
46
56
  extname: '.adoc',
47
57
  },
48
58
  })
49
59
  }
50
60
 
51
61
  function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
52
- const doctitle = navtitle === componentVersion.title ? navtitle : `${componentVersion.title}: ${navtitle}`
62
+ const navtitlePlain = sanitize(navtitle)
63
+ const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
64
+ let doctitle = navtitleAsciiDoc
65
+ if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
53
66
  const version = componentVersion.version === 'master' ? '' : componentVersion.version
54
67
  return [
55
68
  `= ${doctitle}`,
@@ -88,16 +101,26 @@ function aggregateAsciiDoc (
88
101
  asciidocConfig,
89
102
  mutableAttributes,
90
103
  sectionMergeStrategy,
104
+ lastComponentVersion = componentVersion,
91
105
  level = 0
92
106
  ) {
93
107
  const buffer = []
94
108
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
95
109
  if (outlineEntry.hash) return buffer
96
- const { content: navtitle, items, unresolved, urlType, url } = outlineEntry
110
+ const { content: navtitle, items = [], unresolved, urlType, url } = outlineEntry
111
+ const hasItems = items.length > 0
112
+ const navtitlePlain = sanitize(navtitle)
113
+ const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
114
+ const siteUrl = ((val) => {
115
+ if (!val || val === '/') return ''
116
+ return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
117
+ })(asciidocConfig.attributes['site-url'])
97
118
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
98
119
  let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
120
+ if (page && pagesInOutline.aggregated?.includes(page)) page = undefined
99
121
  if (page) {
100
122
  let contents = page.src.contents
123
+ if (contents == null) return buffer
101
124
  // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
102
125
  // IMPORTANT: this must not leave behind lines the parser will drop!
103
126
  // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
@@ -107,16 +130,36 @@ function aggregateAsciiDoc (
107
130
  .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
108
131
  .trimRight()
109
132
  )
133
+ ;(pagesInOutline.aggregated ??= []).push(page)
110
134
  page = new page.constructor(Object.assign({}, page, { contents, mediaType: 'text/asciidoc' }))
111
- const { module: module_, relative, origin } = page.src
135
+ const { component, version, module: module_, relative, origin } = page.src
112
136
  const doc = loadAsciiDoc(page, contentCatalog, asciidocConfig)
113
137
  const refs = doc.getCatalog().refs
114
138
  // NOTE: in Antora, docname is relative src path from module without file extension
115
139
  const docname = doc.getAttribute('docname')
116
140
  const docnameForId = docname.replace(/[/]/g, '::').replace(/[.]/g, '-')
117
- const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docnameForId}:::`
141
+ const scopeId = component !== componentVersion.name
142
+ const idprefix =
143
+ (scopeId ? component + ':' : '') +
144
+ (module_ === 'ROOT' ? (scopeId ? ':' : '') : module_ + ':') +
145
+ docnameForId +
146
+ ':::'
118
147
  buffer.push('')
119
148
  buffer.push(`:docname: ${docname}`)
149
+ if (component !== lastComponentVersion.name) {
150
+ const thisComponentVersion =
151
+ component === componentVersion.name && version === componentVersion.version
152
+ ? componentVersion
153
+ : contentCatalog.getComponentVersion(component, version)
154
+ if (thisComponentVersion) {
155
+ buffer.push(`:page-component-name: ${thisComponentVersion.name}`)
156
+ buffer.push(`:page-component-version:${thisComponentVersion.version ? ' ' + thisComponentVersion.version : ''}`)
157
+ buffer.push(':page-version: {page-component-version}')
158
+ buffer.push(`:page-component-display-version: ${thisComponentVersion.displayVersion}`)
159
+ buffer.push(`:page-component-title: ${thisComponentVersion.title}`)
160
+ lastComponentVersion = thisComponentVersion
161
+ }
162
+ }
120
163
  buffer.push(`:page-module: ${module_}`)
121
164
  buffer.push(`:page-relative-src-path: ${relative}`)
122
165
  //buffer.push(`:page-origin-type: ${origin.type}`)
@@ -128,7 +171,7 @@ function aggregateAsciiDoc (
128
171
  let enclosed
129
172
  // NOTE: if level is 0, doctitle has already been added and we're in the document header
130
173
  if (level) {
131
- if (level === 1 && navtitle === componentVersion.title) {
174
+ if (level === 1 && navtitlePlain === componentVersion.title) {
132
175
  level--
133
176
  } else {
134
177
  let hlevel = level + 1
@@ -138,12 +181,12 @@ function aggregateAsciiDoc (
138
181
  } else {
139
182
  buffer.push(`[#${idprefix}]`)
140
183
  }
141
- buffer.push(`${'='.repeat(hlevel)} ${navtitle}`)
184
+ buffer.push(`${'='.repeat(hlevel)} ${navtitleAsciiDoc}`)
142
185
  }
143
186
  } else {
144
187
  header.unshift(`[#${idprefix}]`)
145
188
  }
146
- if (sectionMergeStrategy === 'enclose' && items && doc.hasSections()) {
189
+ if (sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
147
190
  enclosed = true
148
191
  // TODO: make overview section title configurable
149
192
  //let overviewTitle = doc.getDocumentTitle()
@@ -170,10 +213,6 @@ function aggregateAsciiDoc (
170
213
  buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
171
214
  if (toggleSectids) buffer.push(':sectids:')
172
215
  }
173
- const siteUrl = ((val) => {
174
- if (!val || val === '/') return ''
175
- return val.charAt(val.length - 1) === '/' ? val.substr(0, val.length - 1) : val
176
- })(doc.getAttribute('site-url'))
177
216
  const lines = doc.getSourceLines()
178
217
  const ignoreLines = []
179
218
  // TODO: think more about when multipart is allowed; perhaps configurable
@@ -250,8 +289,8 @@ function aggregateAsciiDoc (
250
289
  let pagePart, fragment, targetPage
251
290
  const hashIdx = target.indexOf('#')
252
291
  if (~hashIdx) {
253
- pagePart = target.substr(0, hashIdx)
254
- fragment = target.substr(hashIdx + 1)
292
+ pagePart = target.slice(0, hashIdx)
293
+ fragment = target.slice(hashIdx + 1)
255
294
  // TODO: for now, assume .adoc; in the future, consider other file extensions
256
295
  if (pagePart && !pagePart.endsWith('.adoc')) pagePart += '.adoc'
257
296
  } else if (target.endsWith('.adoc')) {
@@ -267,20 +306,24 @@ function aggregateAsciiDoc (
267
306
  : `<<${idprefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
268
307
  }
269
308
  if (~pagePart.indexOf('@') || /:.*:/.test(pagePart)) {
309
+ if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
310
+ text ||= targetPage.asciidoc?.xreftext || target
311
+ return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
312
+ }
270
313
  // TODO: handle unresolved page better
271
- return siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out
272
- ? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
273
- : m
314
+ return m
274
315
  } else if (pagePart.indexOf(':') < 0) {
275
316
  if (module_ !== 'ROOT') pagePart = `${module_}:${pagePart}`
276
317
  } else if (pagePart.startsWith('ROOT:')) {
277
- pagePart = pagePart.substr(5)
318
+ pagePart = pagePart.slice(5)
278
319
  }
279
320
  if (!(targetPage = pagesInOutline.get(pagePart))) {
321
+ if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
322
+ text ||= targetPage.asciidoc?.xreftext || target
323
+ return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
324
+ }
280
325
  // TODO: handle unresolved page better
281
- return siteUrl && (targetPage = contentCatalog.resolvePage(target, page.src)) && targetPage.out
282
- ? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
283
- : m
326
+ return m
284
327
  }
285
328
  pagePart = pagePart
286
329
  .replace(/[/]/g, '::')
@@ -301,9 +344,8 @@ function aggregateAsciiDoc (
301
344
  family: 'attachment',
302
345
  relative,
303
346
  })
304
- return attachment && attachment.out
305
- ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]`
306
- : m
347
+ // TODO: handle unresolved attachment page
348
+ return attachment?.out ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]` : m
307
349
  })
308
350
  }
309
351
  if (~line.indexOf('image:') && !line.startsWith('image::')) {
@@ -311,7 +353,7 @@ function aggregateAsciiDoc (
311
353
  if (isResourceSpec(target)) {
312
354
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
313
355
  // TODO: handle (or report) unresolved image better
314
- if (image && image.out) {
356
+ if (image?.out) {
315
357
  image.out.assembled = true
316
358
  return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
317
359
  }
@@ -353,14 +395,14 @@ function aggregateAsciiDoc (
353
395
  let prefix = ''
354
396
  // Q: can we use startsWith('image::') in certain cases?
355
397
  let imageMacroOffset = (
356
- lastImageMacroAt?.[0] === idx ? line.substr(0, lastImageMacroAt[1]) : line
398
+ lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line
357
399
  ).lastIndexOf('image::')
358
400
  if (imageMacroOffset > 0) {
359
401
  if (
360
402
  block.getDocument().isNested() &&
361
- (prefix = line.substr(0, imageMacroOffset)).trimRight().endsWith('|')
403
+ (prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|')
362
404
  ) {
363
- line = line.substr(prefix.length)
405
+ line = line.slice(prefix.length)
364
406
  } else {
365
407
  imageMacroOffset = -1
366
408
  }
@@ -370,8 +412,8 @@ function aggregateAsciiDoc (
370
412
  if (isResourceSpec(target)) {
371
413
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
372
414
  // FIXME: handle (or report) case when image is not resolved
373
- if (image && image.out) {
374
- const boxedAttrlist = line.substr(line.indexOf('['))
415
+ if (image?.out) {
416
+ const boxedAttrlist = line.slice(line.indexOf('['))
375
417
  image.out.assembled = true
376
418
  lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
377
419
  }
@@ -398,7 +440,15 @@ function aggregateAsciiDoc (
398
440
  } else if (val !== initialVal) {
399
441
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
400
442
  }
401
- } else if (val != null && !(doc.isAttributeLocked(name) || name === 'doctype' || name === 'leveloffset')) {
443
+ } else if (
444
+ !(
445
+ val == null ||
446
+ doc.isAttributeLocked(name) ||
447
+ name === 'doctype' ||
448
+ name === 'leveloffset' ||
449
+ name === 'underscore'
450
+ )
451
+ ) {
402
452
  accum.push(`:!${name}:`)
403
453
  }
404
454
  return accum
@@ -407,43 +457,51 @@ function aggregateAsciiDoc (
407
457
  )
408
458
  if (resolvedAttributeEntries.length > 1) buffer.push(...resolvedAttributeEntries)
409
459
  }
410
- } else {
411
- if (level) {
412
- if (level === 1 && navtitle === componentVersion.title) {
413
- level--
414
- } else {
415
- buffer.push('')
416
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
417
- let toggleSectids, syntheticId
418
- if (!('sectids' in asciidocConfig.attributes)) {
460
+ } else if (level) {
461
+ if (level === 1 && navtitlePlain === componentVersion.title) {
462
+ level--
463
+ } else {
464
+ buffer.push('')
465
+ // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
466
+ // Q: should we unset docname, page-module, etc?
467
+ let toggleSectids, syntheticId
468
+ if (!('sectids' in asciidocConfig.attributes)) {
469
+ buffer.push(':!sectids:')
470
+ toggleSectids = true
471
+ } else if (typeof asciidocConfig.attributes.sectids === 'string') {
472
+ if ('sectids' in mutableAttributes) {
419
473
  buffer.push(':!sectids:')
420
474
  toggleSectids = true
421
- } else if (typeof asciidocConfig.attributes.sectids === 'string') {
422
- if ('sectids' in mutableAttributes) {
423
- buffer.push(':!sectids:')
424
- toggleSectids = true
425
- } else {
426
- syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
427
- }
428
- }
429
- // Q: should we unset docname, page-module, etc?
430
- const sectionTitle = urlType === 'external' ? `${url}[${navtitle.replace(/\]/, '\\]')}]` : navtitle
431
- let hlevel = level + 1
432
- if (hlevel > 6) {
433
- hlevel = 6
434
- buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
435
- } else if (syntheticId) {
436
- buffer.push(`[#${syntheticId}]`)
475
+ } else {
476
+ syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
437
477
  }
438
- buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
439
- if (toggleSectids) buffer.push(':sectids:')
440
478
  }
479
+ let sectionTitle = navtitleAsciiDoc
480
+ if (urlType === 'external') {
481
+ sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
482
+ } else if (urlType === 'internal' && !unresolved && siteUrl) {
483
+ const resource = contentCatalog.getFiles().find((it) => it.out && it.pub.url === url)
484
+ if (resource) sectionTitle = `${siteUrl}${resource.pub.url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
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:')
441
495
  }
442
496
  }
443
497
 
444
498
  const nextLevel = level + 1
445
- if (items) {
446
- items.forEach((item) => {
499
+ if (hasItems) {
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) => {
447
505
  buffer.push(
448
506
  ...aggregateAsciiDoc(
449
507
  loadAsciiDoc,
@@ -455,6 +513,7 @@ function aggregateAsciiDoc (
455
513
  asciidocConfig,
456
514
  mutableAttributes,
457
515
  sectionMergeStrategy,
516
+ lastComponentVersion,
458
517
  nextLevel
459
518
  )
460
519
  )
@@ -463,19 +522,12 @@ function aggregateAsciiDoc (
463
522
  return buffer
464
523
  }
465
524
 
466
- function generateStem (componentVersion, title) {
467
- const { name, version } = componentVersion
468
- const segments = []
469
- if (name !== 'ROOT') segments.push(name)
470
- if (version && version !== 'master') segments.push(version)
471
- segments.push(
472
- title
473
- .toLowerCase()
474
- .replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '')
475
- .replace(/[ _.]/g, '-')
476
- .replace(/--+/g, '-')
477
- )
478
- 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, '-')
479
531
  }
480
532
 
481
533
  function fixSectionLevels (sections, multipart) {
@@ -512,10 +564,10 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
512
564
  let rawStyle
513
565
  const commaIdx = prevLine.indexOf(',')
514
566
  if (~commaIdx) {
515
- rawStyle = prevLine.substr(1, commaIdx - 1)
567
+ rawStyle = prevLine.slice(1, commaIdx - 1)
516
568
  if (~rawStyle.indexOf('=')) rawStyle = undefined
517
569
  } else if (!~prevLine.indexOf('=')) {
518
- rawStyle = prevLine.substr(1, prevLine.length - 2)
570
+ rawStyle = prevLine.slice(1, prevLine.length - 2)
519
571
  }
520
572
  if (rawStyle) {
521
573
  if (~rawStyle.indexOf('#')) {
@@ -524,10 +576,10 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
524
576
  } else {
525
577
  prevLine = `[${
526
578
  replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle
527
- }#${idprefix}${block.getId()}${prevLine.substr(rawStyle.length + 1)}`
579
+ }#${idprefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
528
580
  }
529
581
  } else {
530
- prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.substr(1)}`
582
+ prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
531
583
  }
532
584
  if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
533
585
  lines[idx - 1] = prevLine
@@ -5,6 +5,8 @@ const filterComponentVersions = require('./filter-component-versions')
5
5
  const produceAggregateDocument = require('./produce-aggregate-document')
6
6
  const selectMutableAttributes = require('./select-mutable-attributes')
7
7
 
8
+ const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
9
+
8
10
  function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfig) {
9
11
  const { insertStartPage, rootLevel, sectionMergeStrategy, asciidoc: assemblerAsciiDocConfig } = assemblerConfig
10
12
  const assemblerAsciiDocAttributes = Object.assign({}, assemblerAsciiDocConfig.attributes)
@@ -20,26 +22,32 @@ function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfi
20
22
  const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
21
23
  attributes: Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
22
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
34
+ })
23
35
  const rootEntry = { content: title }
24
- const startPage = contentCatalog.getComponentVersionStartPage(componentName, version)
25
- let startPageUrl
26
- if (startPage && insertStartPage) {
27
- if (includedInNav(navigation, (startPageUrl = startPage.pub.url))) {
28
- startPageUrl = undefined
29
- } else {
30
- 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' })
31
40
  }
32
- }
33
- // Q: should we always use a reference page here?
34
- const startPage_ =
35
- startPage ??
36
- createFile({
41
+ } else {
42
+ // Q: should we always use a reference page as startPage for computing mutableAttributes?
43
+ startPage = createFile({
37
44
  component: componentVersion.name,
38
45
  version: componentVersion.version,
39
46
  relative: '.reference-page.adoc',
40
47
  origin: (componentVersion.origins || [])[0],
41
48
  })
42
- const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage_, mergedAsciiDocConfig)
49
+ }
50
+ const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
43
51
  delete mutableAttributes.doctype
44
52
  accum = accum.concat(
45
53
  prepareOutlines(navigation, rootEntry, rootLevel).map((outline) =>
@@ -56,10 +64,10 @@ function produceAggregateDocuments (loadAsciiDoc, contentCatalog, assemblerConfi
56
64
  )
57
65
  )
58
66
  )
59
- mergedAsciiDocConfig.attributes.doctype = doctype
67
+ mergedAsciiDocAttributes.doctype = doctype
60
68
  sourceHighlighter
61
- ? (mergedAsciiDocConfig.attributes['source-highlighter'] = sourceHighlighter)
62
- : delete mergedAsciiDocConfig.attributes['source-highlighter']
69
+ ? (mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter)
70
+ : delete mergedAsciiDocAttributes['source-highlighter']
63
71
  return accum
64
72
  },
65
73
  []
@@ -113,15 +121,22 @@ function includedInNav (items, url) {
113
121
  return items.find((it) => it.url === url || includedInNav(it.items || [], url))
114
122
  }
115
123
 
124
+ function isResourceRef (target) {
125
+ return ~target.indexOf(':') && !(~target.indexOf('://') || (target.startsWith('data:') && ~target.indexOf(',')))
126
+ }
127
+
116
128
  // when root level is 0, merge the navigation into the rootEntry
117
129
  // when root level is 1, create navigation per navigation menu
118
130
  // in this case, if there's only a single navigation menu with no title, promote each top-level item to a menu
119
131
  function prepareOutlines (navigation, rootEntry, rootLevel) {
120
132
  if (rootLevel === 0 || navigation.length === 1) {
121
- const navBranch =
122
- navigation.length === 1
123
- ? navigation[0]
124
- : { items: navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), []) }
133
+ let navBranch
134
+ if (navigation.length === 1) {
135
+ navBranch = navigation[0]
136
+ } else {
137
+ const items = navigation.reduce((accum, it) => accum.concat(it.content ? it : it.items), [])
138
+ navBranch = items.length ? { items } : {}
139
+ }
125
140
  return rootLevel === 0 || navBranch.content ? [Object.assign(rootEntry, navBranch)] : navBranch.items
126
141
  }
127
142
  return navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), [rootEntry])
@@ -0,0 +1,13 @@
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
+ if (~str.indexOf('<')) str = str.replace(XML_TAG_RX, '')
9
+ if (~str.indexOf('&')) str = str.replace(XML_SPECIAL_CHARS_RX, (m) => XML_SPECIAL_CHARS[m])
10
+ return str
11
+ }
12
+
13
+ module.exports = sanitize
@@ -0,0 +1,95 @@
1
+ 'use strict'
2
+
3
+ const ATTRIBUTE_REFERENCE_RX = /\{[a-z0-9_][a-z0-9_-]*\}/g
4
+ const STRICT_WORD_CHAR_RX = /[\p{L}\d_]/u
5
+ const WORD_CHAR_RX = /[\p{L}\d_;}:<>]/u
6
+
7
+ const MARK_FOR_TAG = { code: '`', em: '_', mark: '#', span: '#', strong: '*' }
8
+ const SKIP_SPAN = { icon: '<i ', image: '<img ' }
9
+
10
+ module.exports = (str) => {
11
+ if (!str) return str
12
+ let matchIndex = str.indexOf('<')
13
+ if (!~matchIndex) return ~str.indexOf('{') ? str.replace(ATTRIBUTE_REFERENCE_RX, '\\$&') : str
14
+ let current = { contents: '' }
15
+ const stack = [current]
16
+ let lastIndex = 0
17
+ do {
18
+ if (matchIndex > lastIndex) {
19
+ const matched = str.slice(lastIndex, matchIndex)
20
+ current.contents += ~matched.indexOf('{') ? matched.replace(ATTRIBUTE_REFERENCE_RX, '\\$&') : matched
21
+ }
22
+ const isCloseTag = str[++matchIndex] === '/' ? ++matchIndex : false
23
+ let tagName = str.slice(matchIndex, (lastIndex = str.indexOf('>', matchIndex) + 1) - 1)
24
+ if (isCloseTag) {
25
+ const parent = current // TODO expect tagName to equal current.tagName
26
+ stack.pop()
27
+ current = stack[stack.length - 1]
28
+ if (parent.mark) {
29
+ let { contents, mark, id, role } = parent
30
+ const attrlist = (id ? '#' + id : '') + (role ? '.' + role.replace(/ /g, '.') : '')
31
+ if (
32
+ current.mark === mark ||
33
+ current.mark === '_' ||
34
+ isWordChar(str.charAt(lastIndex), true) ||
35
+ isWordChar(current.contents[current.contents.length - 1] || current.mark)
36
+ ) {
37
+ mark = mark.repeat(2)
38
+ }
39
+ current.contents += (attrlist ? '[' + attrlist + ']' : '') + mark + contents + mark
40
+ } else {
41
+ current.contents += parent.contents
42
+ }
43
+ } else {
44
+ let attrs, attrlistIndex, role
45
+ if (~(attrlistIndex = tagName.indexOf(' '))) {
46
+ role = (attrs = parseAttrlist(tagName.slice(attrlistIndex))).class
47
+ tagName = tagName.slice(0, attrlistIndex)
48
+ }
49
+ if (tagName === 'img') {
50
+ current.contents += 'image:' + attrs.src + '[' + attrs.alt + ']'
51
+ } else if (tagName === 'i' && current.tagName === 'span' && current.role === 'icon') {
52
+ current.contents += 'icon:' + role.slice(6) + '[]'
53
+ lastIndex += 4
54
+ } else {
55
+ let check, mark
56
+ const id = attrs?.id
57
+ if (tagName !== 'span' || id || (role && !((check = SKIP_SPAN[role]) && str.startsWith(check, lastIndex)))) {
58
+ mark = MARK_FOR_TAG[tagName]
59
+ }
60
+ stack.push((current = { tagName, role, id, mark, contents: '' }))
61
+ }
62
+ }
63
+ } while (~(matchIndex = str.indexOf('<', lastIndex)))
64
+ const rest = str.slice(lastIndex)
65
+ if (rest) current.contents += ~rest.indexOf('{') ? rest.replace(ATTRIBUTE_REFERENCE_RX, '\\$&') : rest
66
+ return current.contents
67
+ }
68
+
69
+ function parseAttrlist (str) {
70
+ let lastIndex = 0
71
+ const attrs = {}
72
+ while (str.charAt(lastIndex++) === ' ') {
73
+ const spaceIndex = str.indexOf(' ', lastIndex)
74
+ const equalsIndex = str.indexOf('=', lastIndex)
75
+ if (~spaceIndex && spaceIndex < equalsIndex) {
76
+ attrs[str.slice(lastIndex, (lastIndex = spaceIndex))] = true
77
+ } else if (~equalsIndex) {
78
+ const name = str.slice(lastIndex, equalsIndex)
79
+ const valueIndex = equalsIndex + 1
80
+ attrs[name] =
81
+ str.charAt(valueIndex) === '"'
82
+ ? str.slice(valueIndex + 1, (lastIndex = str.indexOf('"', valueIndex + 1) + 1) - 1)
83
+ : str.slice(valueIndex, (lastIndex = ~spaceIndex ? spaceIndex : str.length))
84
+ } else {
85
+ attrs[str.slice(lastIndex)] = true
86
+ break
87
+ }
88
+ }
89
+ return attrs
90
+ }
91
+
92
+ function isWordChar (str, strict) {
93
+ if (!str) return false
94
+ return (strict ? STRICT_WORD_CHAR_RX : WORD_CHAR_RX).test(str)
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antora/assembler",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.9",
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": {
@@ -28,6 +28,9 @@
28
28
  "./produce-aggregate-documents": "./lib/produce-aggregate-documents.js",
29
29
  "./select-mutable-attributes": "./lib/select-mutable-attributes.js"
30
30
  },
31
+ "imports": {
32
+ "#unconvert-inline-asciidoc": "./lib/util/unconvert-inline-asciidoc.js"
33
+ },
31
34
  "dependencies": {
32
35
  "@antora/expand-path-helper": "~2.0",
33
36
  "braces": "~3.0",
@@ -36,8 +39,8 @@
36
39
  "js-yaml": "~4.1"
37
40
  },
38
41
  "devDependencies": {
39
- "@antora/asciidoc-loader": "3.0.1",
40
- "@antora/site-publisher": "3.0.1"
42
+ "@antora/asciidoc-loader": "~3.1",
43
+ "@antora/site-publisher": "~3.1"
41
44
  },
42
45
  "engines": {
43
46
  "node": ">=16.0.0"