@antora/assembler 1.0.0-alpha.7 → 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)
@@ -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,7 @@
2
2
 
3
3
  const File = require('vinyl')
4
4
  const path = require('node:path/posix')
5
+ const sanitize = require('./util/sanitize')
5
6
 
6
7
  function produceAggregateDocument (
7
8
  loadAsciiDoc,
@@ -14,12 +15,19 @@ function produceAggregateDocument (
14
15
  mutableAttributes,
15
16
  sectionMergeStrategy = 'discrete'
16
17
  ) {
17
- const pagesInOutline = selectPagesInOutline(
18
- outline,
19
- pages.filter((it) => it.out)
20
- )
18
+ const pagesInOutline = selectPagesInOutline(outline, pages)
21
19
  const navtitle = outline.content
22
- 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)
23
31
  const header = buildAsciiDocHeader(componentVersion, navtitle, doctype)
24
32
  const body = aggregateAsciiDoc(
25
33
  loadAsciiDoc,
@@ -32,24 +40,26 @@ function produceAggregateDocument (
32
40
  mutableAttributes,
33
41
  sectionMergeStrategy
34
42
  )
35
- const relativeSrcPath = `${stem}.adoc`
36
43
  return new File({
44
+ aggregate: true,
37
45
  asciidoc: asciidocConfig,
38
46
  contents: Buffer.from([...header, ...body].join('\n') + '\n'),
39
47
  mediaType: 'text/asciidoc',
40
- path: relativeSrcPath,
48
+ path: path_,
41
49
  src: {
42
50
  component: componentVersion.name,
43
51
  version: componentVersion.version,
44
- basename: path.basename(relativeSrcPath),
45
- stem,
52
+ basename: path.basename(path_),
53
+ stem: path.basename(path_, '.adoc'),
46
54
  extname: '.adoc',
47
55
  },
48
56
  })
49
57
  }
50
58
 
51
59
  function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
52
- 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}`
53
63
  const version = componentVersion.version === 'master' ? '' : componentVersion.version
54
64
  return [
55
65
  `= ${doctitle}`,
@@ -88,16 +98,24 @@ function aggregateAsciiDoc (
88
98
  asciidocConfig,
89
99
  mutableAttributes,
90
100
  sectionMergeStrategy,
101
+ lastComponentVersion = componentVersion,
91
102
  level = 0
92
103
  ) {
93
104
  const buffer = []
94
105
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
95
106
  if (outlineEntry.hash) return buffer
96
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'])
97
113
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
98
114
  let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
115
+ if (page && pagesInOutline.aggregated?.includes(page)) page = undefined
99
116
  if (page) {
100
117
  let contents = page.src.contents
118
+ if (contents == null) return buffer
101
119
  // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
102
120
  // IMPORTANT: this must not leave behind lines the parser will drop!
103
121
  // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
@@ -107,16 +125,36 @@ function aggregateAsciiDoc (
107
125
  .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
108
126
  .trimRight()
109
127
  )
128
+ ;(pagesInOutline.aggregated ??= []).push(page)
110
129
  page = new page.constructor(Object.assign({}, page, { contents, mediaType: 'text/asciidoc' }))
111
- const { module: module_, relative, origin } = page.src
130
+ const { component, version, module: module_, relative, origin } = page.src
112
131
  const doc = loadAsciiDoc(page, contentCatalog, asciidocConfig)
113
132
  const refs = doc.getCatalog().refs
114
133
  // NOTE: in Antora, docname is relative src path from module without file extension
115
134
  const docname = doc.getAttribute('docname')
116
135
  const docnameForId = docname.replace(/[/]/g, '::').replace(/[.]/g, '-')
117
- const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docnameForId}:::`
136
+ const scopeId = component !== componentVersion.name
137
+ const idprefix =
138
+ (scopeId ? component + ':' : '') +
139
+ (module_ === 'ROOT' ? (scopeId ? ':' : '') : module_ + ':') +
140
+ docnameForId +
141
+ ':::'
118
142
  buffer.push('')
119
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
+ }
120
158
  buffer.push(`:page-module: ${module_}`)
121
159
  buffer.push(`:page-relative-src-path: ${relative}`)
122
160
  //buffer.push(`:page-origin-type: ${origin.type}`)
@@ -128,7 +166,7 @@ function aggregateAsciiDoc (
128
166
  let enclosed
129
167
  // NOTE: if level is 0, doctitle has already been added and we're in the document header
130
168
  if (level) {
131
- if (level === 1 && navtitle === componentVersion.title) {
169
+ if (level === 1 && navtitlePlain === componentVersion.title) {
132
170
  level--
133
171
  } else {
134
172
  let hlevel = level + 1
@@ -138,7 +176,7 @@ function aggregateAsciiDoc (
138
176
  } else {
139
177
  buffer.push(`[#${idprefix}]`)
140
178
  }
141
- buffer.push(`${'='.repeat(hlevel)} ${navtitle}`)
179
+ buffer.push(`${'='.repeat(hlevel)} ${navtitleAsciiDoc}`)
142
180
  }
143
181
  } else {
144
182
  header.unshift(`[#${idprefix}]`)
@@ -170,10 +208,6 @@ function aggregateAsciiDoc (
170
208
  buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
171
209
  if (toggleSectids) buffer.push(':sectids:')
172
210
  }
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
211
  const lines = doc.getSourceLines()
178
212
  const ignoreLines = []
179
213
  // TODO: think more about when multipart is allowed; perhaps configurable
@@ -250,8 +284,8 @@ function aggregateAsciiDoc (
250
284
  let pagePart, fragment, targetPage
251
285
  const hashIdx = target.indexOf('#')
252
286
  if (~hashIdx) {
253
- pagePart = target.substr(0, hashIdx)
254
- fragment = target.substr(hashIdx + 1)
287
+ pagePart = target.slice(0, hashIdx)
288
+ fragment = target.slice(hashIdx + 1)
255
289
  // TODO: for now, assume .adoc; in the future, consider other file extensions
256
290
  if (pagePart && !pagePart.endsWith('.adoc')) pagePart += '.adoc'
257
291
  } else if (target.endsWith('.adoc')) {
@@ -267,20 +301,24 @@ function aggregateAsciiDoc (
267
301
  : `<<${idprefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
268
302
  }
269
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
+ }
270
308
  // 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
309
+ return m
274
310
  } else if (pagePart.indexOf(':') < 0) {
275
311
  if (module_ !== 'ROOT') pagePart = `${module_}:${pagePart}`
276
312
  } else if (pagePart.startsWith('ROOT:')) {
277
- pagePart = pagePart.substr(5)
313
+ pagePart = pagePart.slice(5)
278
314
  }
279
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
+ }
280
320
  // 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
321
+ return m
284
322
  }
285
323
  pagePart = pagePart
286
324
  .replace(/[/]/g, '::')
@@ -301,6 +339,7 @@ function aggregateAsciiDoc (
301
339
  family: 'attachment',
302
340
  relative,
303
341
  })
342
+ // TODO: handle unresolved attachment page
304
343
  return attachment && attachment.out
305
344
  ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]`
306
345
  : m
@@ -353,14 +392,14 @@ function aggregateAsciiDoc (
353
392
  let prefix = ''
354
393
  // Q: can we use startsWith('image::') in certain cases?
355
394
  let imageMacroOffset = (
356
- lastImageMacroAt?.[0] === idx ? line.substr(0, lastImageMacroAt[1]) : line
395
+ lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line
357
396
  ).lastIndexOf('image::')
358
397
  if (imageMacroOffset > 0) {
359
398
  if (
360
399
  block.getDocument().isNested() &&
361
- (prefix = line.substr(0, imageMacroOffset)).trimRight().endsWith('|')
400
+ (prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|')
362
401
  ) {
363
- line = line.substr(prefix.length)
402
+ line = line.slice(prefix.length)
364
403
  } else {
365
404
  imageMacroOffset = -1
366
405
  }
@@ -371,7 +410,7 @@ function aggregateAsciiDoc (
371
410
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
372
411
  // FIXME: handle (or report) case when image is not resolved
373
412
  if (image && image.out) {
374
- const boxedAttrlist = line.substr(line.indexOf('['))
413
+ const boxedAttrlist = line.slice(line.indexOf('['))
375
414
  image.out.assembled = true
376
415
  lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
377
416
  }
@@ -398,7 +437,15 @@ function aggregateAsciiDoc (
398
437
  } else if (val !== initialVal) {
399
438
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
400
439
  }
401
- } 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
+ ) {
402
449
  accum.push(`:!${name}:`)
403
450
  }
404
451
  return accum
@@ -407,43 +454,54 @@ function aggregateAsciiDoc (
407
454
  )
408
455
  if (resolvedAttributeEntries.length > 1) buffer.push(...resolvedAttributeEntries)
409
456
  }
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)) {
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) {
419
470
  buffer.push(':!sectids:')
420
471
  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
- }
472
+ } else {
473
+ syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
428
474
  }
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
+ }
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}]`
437
484
  }
438
- buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
439
- if (toggleSectids) buffer.push(':sectids:')
440
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
499
  if (items) {
446
- 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) => {
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,6 +121,10 @@ 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
@@ -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.7",
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": {
@@ -36,8 +36,8 @@
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"