@antora/assembler 1.0.0-alpha.1 → 1.0.0-alpha.11

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.
@@ -1,13 +1,15 @@
1
1
  'use strict'
2
2
 
3
- const File = require('vinyl')
4
- const { posix: path } = require('path')
3
+ const path = require('node:path/posix')
4
+ const sanitize = require('./util/sanitize')
5
+ const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
5
6
 
6
7
  function produceAggregateDocument (
7
8
  loadAsciiDoc,
8
9
  contentCatalog,
9
10
  componentVersion,
10
11
  outline,
12
+ doctype,
11
13
  pages,
12
14
  asciidocConfig,
13
15
  mutableAttributes,
@@ -15,8 +17,19 @@ function produceAggregateDocument (
15
17
  ) {
16
18
  const pagesInOutline = selectPagesInOutline(outline, pages)
17
19
  const navtitle = outline.content
18
- const stem = generateStem(componentVersion, navtitle)
19
- const header = buildAsciiDocHeader(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 { dir: outDir, name: outName } = path.parse(templateFile.out.path)
30
+ const path_ = outName === templateFile.src.relative ? path.join(outDir, outName + '.adoc') : outDir + '.adoc'
31
+ contentCatalog.removeFile(templateFile)
32
+ const header = buildAsciiDocHeader(componentVersion, navtitle, doctype)
20
33
  const body = aggregateAsciiDoc(
21
34
  loadAsciiDoc,
22
35
  contentCatalog,
@@ -28,29 +41,33 @@ function produceAggregateDocument (
28
41
  mutableAttributes,
29
42
  sectionMergeStrategy
30
43
  )
31
- const relativeSrcPath = `${stem}.adoc`
32
- return new File({
44
+ return new templateFile.constructor({
45
+ aggregate: true,
33
46
  asciidoc: asciidocConfig,
34
47
  contents: Buffer.from([...header, ...body].join('\n') + '\n'),
35
48
  mediaType: 'text/asciidoc',
36
- path: relativeSrcPath,
49
+ path: path_,
37
50
  src: {
38
51
  component: componentVersion.name,
39
52
  version: componentVersion.version,
40
- basename: path.basename(relativeSrcPath),
41
- stem,
53
+ basename: path.basename(path_),
54
+ stem: path.basename(path_, '.adoc'),
42
55
  extname: '.adoc',
43
56
  },
44
57
  })
45
58
  }
46
59
 
47
- function buildAsciiDocHeader (componentVersion, navtitle) {
48
- const doctitle = navtitle === componentVersion.title ? navtitle : `${componentVersion.title}: ${navtitle}`
49
- const version = componentVersion.version && componentVersion.version !== 'master' ? componentVersion.version : ''
60
+ function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
61
+ const navtitlePlain = sanitize(navtitle)
62
+ const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
63
+ let doctitle = navtitleAsciiDoc
64
+ if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
65
+ const version = componentVersion.version === 'master' ? '' : componentVersion.version
50
66
  return [
51
67
  `= ${doctitle}`,
52
- ...(version ? [`v${version}`] : []),
53
- ':doctype: book', // for debugging only; set via CLI
68
+ ...(version ? [`:revnumber: ${version}`] : []),
69
+ ...(doctype === 'article' ? [] : [`:doctype: ${doctype}`]),
70
+ ':underscore: _',
54
71
  // Q: should we pass these via the CLI so they cannot be modified?
55
72
  `:page-component-name: ${componentVersion.name}`,
56
73
  `:page-component-version:${version ? ' ' + version : ''}`,
@@ -63,7 +80,7 @@ function buildAsciiDocHeader (componentVersion, navtitle) {
63
80
  function selectPagesInOutline (outlineEntry, pages) {
64
81
  const page = outlineEntry.urlType === 'internal' ? pages.find((it) => it.pub.url === outlineEntry.url) : undefined
65
82
  return (outlineEntry.items || []).reduce(
66
- (accum, item) => new Map([...accum, ...selectPagesInOutline(item, pages)]),
83
+ (accum, item) => selectPagesInOutline(item, pages).forEach((v, k) => accum.set(k, v)) ?? accum,
67
84
  new Map(
68
85
  page && [
69
86
  [`${page.src.module === 'ROOT' ? '' : page.src.module + ':'}${page.src.relative}`, page],
@@ -83,16 +100,26 @@ function aggregateAsciiDoc (
83
100
  asciidocConfig,
84
101
  mutableAttributes,
85
102
  sectionMergeStrategy,
103
+ lastComponentVersion = componentVersion,
86
104
  level = 0
87
105
  ) {
88
106
  const buffer = []
89
107
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
90
108
  if (outlineEntry.hash) return buffer
91
- const { content: navtitle, items, unresolved, urlType, url } = outlineEntry
109
+ const { content: navtitle, items = [], unresolved, urlType, url } = outlineEntry
110
+ const hasItems = items.length > 0
111
+ const navtitlePlain = sanitize(navtitle)
112
+ const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
113
+ const siteUrl = ((val) => {
114
+ if (!val || val === '/') return ''
115
+ return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
116
+ })(asciidocConfig.attributes['site-url'])
92
117
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
93
118
  let page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
119
+ if (page && pagesInOutline.aggregated?.includes(page)) page = undefined
94
120
  if (page) {
95
121
  let contents = page.src.contents
122
+ if (contents == null) return buffer
96
123
  // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
97
124
  // IMPORTANT: this must not leave behind lines the parser will drop!
98
125
  // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
@@ -102,17 +129,37 @@ function aggregateAsciiDoc (
102
129
  .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
103
130
  .trimRight()
104
131
  )
132
+ ;(pagesInOutline.aggregated ??= []).push(page)
105
133
  page = new page.constructor(Object.assign({}, page, { contents, mediaType: 'text/asciidoc' }))
106
- const { module: module_, relative, origin } = page.src
134
+ const { component, version, module: module_, relative, origin } = page.src
135
+ const topicPrefix = ~relative.indexOf('/') ? path.dirname(relative) + '/' : ''
107
136
  const doc = loadAsciiDoc(page, contentCatalog, asciidocConfig)
108
- const ids = doc.getCatalog().ids
137
+ const refs = doc.getCatalog().refs
109
138
  // NOTE: in Antora, docname is relative src path from module without file extension
110
139
  const docname = doc.getAttribute('docname')
111
- //const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docname}//`
112
- const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docname.replace(/[/]/g, '::')}:::`
113
- //const idprefix = `${module_ === 'ROOT' ? '' : module_ + ':'}${docname.replace(/[/]/g, ':-:')}:--:`
140
+ const docnameForId = docname.replace(/[/]/g, '::').replace(/[.]/g, '-')
141
+ const scopeId = component !== componentVersion.name
142
+ const idprefix =
143
+ (scopeId ? component + ':' : '') +
144
+ (module_ === 'ROOT' ? (scopeId ? ':' : '') : module_ + ':') +
145
+ docnameForId +
146
+ ':::'
114
147
  buffer.push('')
115
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
+ }
116
163
  buffer.push(`:page-module: ${module_}`)
117
164
  buffer.push(`:page-relative-src-path: ${relative}`)
118
165
  //buffer.push(`:page-origin-type: ${origin.type}`)
@@ -124,7 +171,7 @@ function aggregateAsciiDoc (
124
171
  let enclosed
125
172
  // NOTE: if level is 0, doctitle has already been added and we're in the document header
126
173
  if (level) {
127
- if (level === 1 && navtitle === componentVersion.title) {
174
+ if (level === 1 && navtitlePlain === componentVersion.title) {
128
175
  level--
129
176
  } else {
130
177
  let hlevel = level + 1
@@ -134,12 +181,12 @@ function aggregateAsciiDoc (
134
181
  } else {
135
182
  buffer.push(`[#${idprefix}]`)
136
183
  }
137
- buffer.push(`${'='.repeat(hlevel)} ${navtitle}`)
184
+ buffer.push(`${'='.repeat(hlevel)} ${navtitleAsciiDoc}`)
138
185
  }
139
186
  } else {
140
187
  header.unshift(`[#${idprefix}]`)
141
188
  }
142
- if (sectionMergeStrategy === 'enclose' && items && doc.hasSections()) {
189
+ if (sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
143
190
  enclosed = true
144
191
  // TODO: make overview section title configurable
145
192
  //let overviewTitle = doc.getDocumentTitle()
@@ -166,27 +213,15 @@ function aggregateAsciiDoc (
166
213
  buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
167
214
  if (toggleSectids) buffer.push(':sectids:')
168
215
  }
169
- const siteUrl = doc.getAttribute('site-url')
170
216
  const lines = doc.getSourceLines()
171
217
  const ignoreLines = []
172
218
  // TODO: think more about when multipart is allowed; perhaps configurable
173
219
  if (doc.hasSections()) fixSectionLevels(doc.getSections(), level === 0)
174
- // TODO: update / simplify findBy calls when upgrading to Asciidoctor 2
175
- const allBlocks = doc
176
- .findBy((it) => it.getContext() !== 'document')
177
- .reduce((accum, block) => {
178
- accum.push(block)
179
- if (block.getContext() === 'table') {
180
- const rows = block.rows
181
- ;[...rows.body, ...rows.foot].forEach((row) => {
182
- row.forEach((cell) => {
183
- if (cell.style !== 'asciidoc') return
184
- accum.push(...cell.inner_document.findBy((it) => it.getContext() !== 'document'))
185
- })
186
- })
187
- }
188
- return accum
189
- }, [])
220
+ const allBlocks = doc.findBy({ traverse_documents: true }, (it) =>
221
+ it.getContext() === 'document'
222
+ ? it.getDocument().isNested()
223
+ : !(it.getContext() === 'table_cell' && it.getStyle() === 'asciidoc')
224
+ )
190
225
  allBlocks.forEach((block) => {
191
226
  const contentModel = block.content_model
192
227
  if (
@@ -213,16 +248,33 @@ function aggregateAsciiDoc (
213
248
  for (let i = idx; i < block.lines.length + (delimited ? idx + 2 : idx); i++) ignoreLines.push(i)
214
249
  }
215
250
  })
251
+ let skipping
216
252
  for (let idx = 0, len = lines.length; idx < len; idx++) {
217
253
  if (~ignoreLines.indexOf(idx)) continue
218
254
  let line = lines[idx]
255
+ if (line.startsWith('//')) {
256
+ if (line[2] !== '/') continue
257
+ if (line.length > 3 && line === '/'.repeat(line.length)) {
258
+ if (skipping) {
259
+ if (line === skipping) skipping = undefined
260
+ } else {
261
+ skipping = line
262
+ }
263
+ continue
264
+ }
265
+ } else if (skipping) {
266
+ continue
267
+ }
268
+ if (line.charAt() === ':' && /^:(?:leveloffset: .*|!leveloffset:|leveloffset!:)$/.test(line)) {
269
+ if (lines[idx - 1] === '') lines[idx - 1] = undefined
270
+ lines[idx] = undefined
271
+ continue
272
+ }
219
273
  if (~line.indexOf('<<')) {
220
274
  line = line.replace(/(?<![\\+])<<#?([\p{Alpha}0-9_/.:{][^>,]*?)(?:|, *([^>]+?))?>>/gu, (m, refid, text) => {
221
- // support natural xref; note this logic will change when upgrading to Asciidoctor 2
222
- if (!ids['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
223
- if (!(refid = doc.getCatalog().ids.$key(refid).$to_s())) {
224
- return m
225
- }
275
+ // support natural xref
276
+ if (!refs['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
277
+ if ((refid = doc.$resolve_id(refid))['$nil?']()) return m
226
278
  }
227
279
  return `<<${idprefix}${refid}${text ? ',' + text : ''}>>`
228
280
  })
@@ -233,38 +285,56 @@ function aggregateAsciiDoc (
233
285
  }
234
286
  if (~line.indexOf('xref:')) {
235
287
  // Q: should we allow : as first character of target?
236
- line = line.replace(/xref:([\p{Alpha}#/.{].*?)\[(|.*?[^\\])\]/gu, (m, target, text) => {
288
+ line = line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|.*?[^\\])\]/gu, (m, target, text) => {
237
289
  let pagePart, fragment, targetPage
238
290
  const hashIdx = target.indexOf('#')
239
291
  if (~hashIdx) {
240
- pagePart = target.substr(0, hashIdx)
241
- fragment = target.substr(hashIdx + 1)
292
+ pagePart = target.slice(0, hashIdx)
293
+ fragment = target.slice(hashIdx + 1)
242
294
  // TODO: for now, assume .adoc; in the future, consider other file extensions
243
- if (!(pagePart && pagePart.endsWith('.adoc'))) pagePart += '.adoc'
295
+ if (pagePart && !pagePart.endsWith('.adoc')) pagePart += '.adoc'
244
296
  } else if (target.endsWith('.adoc')) {
245
297
  pagePart = target
246
298
  fragment = ''
247
299
  } else {
248
300
  fragment = target
249
301
  }
250
- if (!pagePart) return `<<${idprefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
302
+ if (!pagePart) {
303
+ // Q: should we validate the internal ID here?
304
+ return text && ~text.indexOf('=')
305
+ ? `xref:${idprefix}${fragment}[${text}]`
306
+ : `<<${idprefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
307
+ }
251
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
+ }
252
313
  // TODO: handle unresolved page better
253
- return siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src))
254
- ? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
255
- : m
256
- } else if (pagePart.indexOf(':') < 0) {
257
- if (module_ !== 'ROOT') pagePart = `${module_}:${pagePart}`
258
- } else if (pagePart.startsWith('ROOT:')) {
259
- pagePart = pagePart.substr(5)
314
+ return m
315
+ }
316
+ let targetModule
317
+ const colonIdx = pagePart.indexOf(':')
318
+ if (~colonIdx) {
319
+ targetModule = pagePart.slice(0, colonIdx)
320
+ pagePart = pagePart.slice(colonIdx + 1)
321
+ } else {
322
+ targetModule = module_
260
323
  }
324
+ if (pagePart.startsWith('./')) pagePart = topicPrefix + pagePart.slice(2)
325
+ if (targetModule !== 'ROOT') pagePart = `${targetModule}:${pagePart}`
261
326
  if (!(targetPage = pagesInOutline.get(pagePart))) {
327
+ if (siteUrl && (targetPage = contentCatalog.resolvePage(pagePart, page.src)) && targetPage.out) {
328
+ text ||= targetPage.asciidoc?.xreftext || target
329
+ return `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
330
+ }
262
331
  // TODO: handle unresolved page better
263
- return siteUrl && (targetPage = contentCatalog.resolvePage(target, page.src))
264
- ? `${siteUrl}${targetPage.pub.url}${fragment && '#' + fragment}[${text}]`
265
- : m
332
+ return m
266
333
  }
267
- pagePart = pagePart.replace(/[/]/g, '::').replace(/\.adoc$/, '')
334
+ pagePart = pagePart
335
+ .replace(/[/]/g, '::')
336
+ .replace(/\.adoc$/, '')
337
+ .replace(/[.]/g, '-')
268
338
  const refid = `${pagePart}:::${fragment}`
269
339
  return `<<${refid}${text && text !== targetPage.title ? ',' + text.replace(/\\]/g, ']') : ''}>>`
270
340
  })
@@ -280,7 +350,8 @@ function aggregateAsciiDoc (
280
350
  family: 'attachment',
281
351
  relative,
282
352
  })
283
- return attachment ? `${siteUrl}${attachment.pub.url}[${text}]` : m
353
+ // TODO: handle unresolved attachment page
354
+ return attachment?.out ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]` : m
284
355
  })
285
356
  }
286
357
  if (~line.indexOf('image:') && !line.startsWith('image::')) {
@@ -288,9 +359,9 @@ function aggregateAsciiDoc (
288
359
  if (isResourceSpec(target)) {
289
360
  const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
290
361
  // TODO: handle (or report) unresolved image better
291
- if (image) {
362
+ if (image?.out) {
292
363
  image.out.assembled = true
293
- return `image:${image.pub.url.substr(1)}[${attrlist}]`
364
+ return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
294
365
  }
295
366
  }
296
367
  return m
@@ -301,66 +372,69 @@ function aggregateAsciiDoc (
301
372
  // NOTE: need to do this last since it modifies the line numbers
302
373
  // we could remap the line numbers to make them resilient
303
374
  // or we could mark which lines to remove and filter them after
375
+ let lastImageMacroAt
304
376
  ;[...allBlocks].reverse().forEach((block) => {
305
377
  const lineno = block.getLineNumber()
306
378
  // NOTE: lineno is not defined for preamble
307
379
  if (typeof lineno !== 'number') return
308
380
  const context = block.getContext()
309
- const idx = lineno - 1
310
- if (context === 'section') {
381
+ let idx = lineno - 1
382
+ if (context === 'section' && !block.getDocument().isNested()) {
311
383
  if (block.getSectionName() === 'header') {
312
- lines.splice(idx, 1)
384
+ lines[idx] = undefined
313
385
  return
314
386
  }
315
387
  let blockStyle = sectionMergeStrategy === 'discrete' ? 'discrete' : undefined
316
- // FIXME: quick fix; needs more thorough review
317
- const leveloffset = Number(doc.getAttribute('leveloffset') || 0)
318
- lines[idx] = lines[idx].replace(/^=+( .+)/, (_, rest) => {
319
- let targetMarkerLength = block.level + (1 - leveloffset) + level + (enclosed ? 1 : 0)
388
+ lines[idx] = lines[idx].replace(/^=+ (.+)/, (_, rest) => {
389
+ let targetMarkerLength = block.level + 1 + level + (enclosed ? 1 : 0)
320
390
  if (targetMarkerLength > 6) {
321
391
  targetMarkerLength = 6
322
392
  blockStyle = 'discrete'
323
393
  }
324
- return '='.repeat(targetMarkerLength) + rest
394
+ return '='.repeat(targetMarkerLength) + ' ' + rest
325
395
  })
326
396
  // NOTE: ID will be undefined if sectids are turned off
327
397
  if (block.getId()) rewriteStyleAttribute(block, lines, idx, idprefix, blockStyle)
328
398
  } else {
329
399
  if (context === 'image') {
330
- const atImageMacro = (lines[idx] || '').startsWith('image::')
331
- // NOTE: the following logic is needed only if parser is messing up line number of image block
332
- //let atImageMacro
333
- //// NOTE: account for line number tracking error in parser when image has block anchor
334
- //if (block.getId()) {
335
- // const originalIdx = idx
336
- // let line
337
- // while ((line = lines[idx]) != null && !(atImageMacro = line.startsWith('image::'))) idx++
338
- // if (!atImageMacro) {
339
- // idx = originalIdx
340
- // if ((lines[idx - 1] || '').startsWith('image::')) {
341
- // atImageMacro = true
342
- // idx--
343
- // }
344
- // }
345
- //} else if ((lines[idx] || '').startsWith('image::')) {
346
- // atImageMacro = true
347
- //}
348
- const target = block.getAttribute('target')
349
- if (atImageMacro && isResourceSpec(target)) {
350
- const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
351
- // FIXME: handle (or report) case when image is not resolved
352
- if (image) {
353
- image.out.assembled = true
354
- const line = lines[idx]
355
- lines[idx] = `image::${image.pub.url.substr(1)}${line.substr(line.indexOf('['))}`
400
+ let line = lines[idx] || ''
401
+ let prefix = ''
402
+ // Q: can we use startsWith('image::') in certain cases?
403
+ let imageMacroOffset = (
404
+ lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line
405
+ ).lastIndexOf('image::')
406
+ if (imageMacroOffset > 0) {
407
+ if (
408
+ block.getDocument().isNested() &&
409
+ (prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|')
410
+ ) {
411
+ line = line.slice(prefix.length)
412
+ } else {
413
+ imageMacroOffset = -1
414
+ }
415
+ }
416
+ if (imageMacroOffset >= 0) {
417
+ const target = block.getAttribute('target')
418
+ if (isResourceSpec(target)) {
419
+ const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
420
+ // FIXME: handle (or report) case when image is not resolved
421
+ if (image?.out) {
422
+ const boxedAttrlist = line.slice(line.indexOf('['))
423
+ image.out.assembled = true
424
+ lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
425
+ }
356
426
  }
427
+ lastImageMacroAt = [idx, imageMacroOffset]
357
428
  }
429
+ } else if (context === 'document' && block.hasHeader()) {
430
+ // nested document
431
+ idx = (block.getHeader().getLineNumber() || idx + 1) - 1
358
432
  }
359
433
  if (block.getId()) rewriteStyleAttribute(block, lines, idx, idprefix)
360
434
  }
361
435
  })
362
- buffer.push(...lines)
363
- const attributeEntries = Object.entries(doc.attributes_defined_in_header || {})
436
+ buffer.push(...lines.filter((it) => it !== undefined))
437
+ const attributeEntries = Object.entries(doc.source_header_attributes?.$$smap || {})
364
438
  if (attributeEntries.length) {
365
439
  const resolvedAttributeEntries = attributeEntries.reduce(
366
440
  (accum, [name, val]) => {
@@ -372,7 +446,15 @@ function aggregateAsciiDoc (
372
446
  } else if (val !== initialVal) {
373
447
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
374
448
  }
375
- } else if (val != null && !doc.isAttributeLocked(name)) {
449
+ } else if (
450
+ !(
451
+ val == null ||
452
+ doc.isAttributeLocked(name) ||
453
+ name === 'doctype' ||
454
+ name === 'leveloffset' ||
455
+ name === 'underscore'
456
+ )
457
+ ) {
376
458
  accum.push(`:!${name}:`)
377
459
  }
378
460
  return accum
@@ -381,43 +463,51 @@ function aggregateAsciiDoc (
381
463
  )
382
464
  if (resolvedAttributeEntries.length > 1) buffer.push(...resolvedAttributeEntries)
383
465
  }
384
- } else {
385
- if (level) {
386
- if (level === 1 && navtitle === componentVersion.title) {
387
- level--
388
- } else {
389
- buffer.push('')
390
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
391
- let toggleSectids, syntheticId
392
- if (!('sectids' in asciidocConfig.attributes)) {
466
+ } else if (level) {
467
+ if (level === 1 && navtitlePlain === componentVersion.title) {
468
+ level--
469
+ } else {
470
+ buffer.push('')
471
+ // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
472
+ // Q: should we unset docname, page-module, etc?
473
+ let toggleSectids, syntheticId
474
+ if (!('sectids' in asciidocConfig.attributes)) {
475
+ buffer.push(':!sectids:')
476
+ toggleSectids = true
477
+ } else if (typeof asciidocConfig.attributes.sectids === 'string') {
478
+ if ('sectids' in mutableAttributes) {
393
479
  buffer.push(':!sectids:')
394
480
  toggleSectids = true
395
- } else if (typeof asciidocConfig.attributes.sectids === 'string') {
396
- if ('sectids' in mutableAttributes) {
397
- buffer.push(':!sectids:')
398
- toggleSectids = true
399
- } else {
400
- syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
401
- }
402
- }
403
- // Q: should we unset docname, page-module, etc?
404
- const sectionTitle = urlType === 'external' ? `${url}[${navtitle.replace(/\]/, '\\]')}]` : navtitle
405
- let hlevel = level + 1
406
- if (hlevel > 6) {
407
- hlevel = 6
408
- buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
409
- } else if (syntheticId) {
410
- buffer.push(`[#${syntheticId}]`)
481
+ } else {
482
+ syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
411
483
  }
412
- buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
413
- if (toggleSectids) buffer.push(':sectids:')
414
484
  }
485
+ let sectionTitle = navtitleAsciiDoc
486
+ if (urlType === 'external') {
487
+ sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
488
+ } else if (urlType === 'internal' && !unresolved && siteUrl) {
489
+ const resource = contentCatalog.getFiles().find((it) => it.out && it.pub.url === url)
490
+ if (resource) sectionTitle = `${siteUrl}${resource.pub.url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
491
+ }
492
+ let hlevel = level + 1
493
+ if (hlevel > 6) {
494
+ hlevel = 6
495
+ buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
496
+ } else if (syntheticId) {
497
+ buffer.push(`[#${syntheticId}]`)
498
+ }
499
+ buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
500
+ if (toggleSectids) buffer.push(':sectids:')
415
501
  }
416
502
  }
417
503
 
418
504
  const nextLevel = level + 1
419
- if (items) {
420
- items.forEach((item) => {
505
+ if (hasItems) {
506
+ // NOTE: drop first child if same as parent; should we keep if content is different?
507
+ ;(urlType === 'internal' && urlType === items[0].urlType && url === items[0].url && !items[0].items
508
+ ? items.slice(1)
509
+ : items
510
+ ).forEach((item) => {
421
511
  buffer.push(
422
512
  ...aggregateAsciiDoc(
423
513
  loadAsciiDoc,
@@ -429,6 +519,7 @@ function aggregateAsciiDoc (
429
519
  asciidocConfig,
430
520
  mutableAttributes,
431
521
  sectionMergeStrategy,
522
+ lastComponentVersion,
432
523
  nextLevel
433
524
  )
434
525
  )
@@ -437,18 +528,12 @@ function aggregateAsciiDoc (
437
528
  return buffer
438
529
  }
439
530
 
440
- function generateStem (componentVersion, title) {
441
- const { name, version } = componentVersion
442
- const segments = [name]
443
- if (version && version !== 'master') segments.push(version)
444
- segments.push(
445
- title
446
- .toLowerCase()
447
- .replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '')
448
- .replace(/[ _.]/g, '-')
449
- .replace(/--+/g, '-')
450
- )
451
- return path.join(...segments)
531
+ function generateSlug (title) {
532
+ return title
533
+ .toLowerCase()
534
+ .replace(/&.+?;|[^ \p{Alpha}0-9_\-.]/gu, '')
535
+ .replace(/[ _.]/g, '-')
536
+ .replace(/--+/g, '-')
452
537
  }
453
538
 
454
539
  function fixSectionLevels (sections, multipart) {
@@ -485,10 +570,10 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
485
570
  let rawStyle
486
571
  const commaIdx = prevLine.indexOf(',')
487
572
  if (~commaIdx) {
488
- rawStyle = prevLine.substr(1, commaIdx - 1)
573
+ rawStyle = prevLine.slice(1, commaIdx)
489
574
  if (~rawStyle.indexOf('=')) rawStyle = undefined
490
575
  } else if (!~prevLine.indexOf('=')) {
491
- rawStyle = prevLine.substr(1, prevLine.length - 2)
576
+ rawStyle = prevLine.slice(1, prevLine.length - 1)
492
577
  }
493
578
  if (rawStyle) {
494
579
  if (~rawStyle.indexOf('#')) {
@@ -496,11 +581,11 @@ function rewriteStyleAttribute (block, lines, idx, idprefix, replacementStyle =
496
581
  if (replacementStyle) prevLine = prevLine.replace(/^[^#.%,\]]+/, `[${replacementStyle}`)
497
582
  } else {
498
583
  prevLine = `[${
499
- replacementStyle ? rawStyle.replace(/^[^.%]*]/, replacementStyle) : rawStyle
500
- }#${idprefix}${block.getId()}${prevLine.substr(rawStyle.length + 1)}`
584
+ replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle
585
+ }#${idprefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
501
586
  }
502
587
  } else {
503
- prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.substr(1)}`
588
+ prevLine = `[${replacementStyle}#${idprefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
504
589
  }
505
590
  if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
506
591
  lines[idx - 1] = prevLine